agora inbox for pgsql-hackers@postgresql.orghelp / color / mirror / Atom feed
[PATCH v7 2/7] Row pattern recognition patch (parse/analysis). 330+ messages / 2 participants [nested] [flat]
* [PATCH v7 2/7] Row pattern recognition patch (parse/analysis). @ 2023-09-22 04:53 Tatsuo Ishii <ishii@postgresql.org> 0 siblings, 0 replies; 330+ messages in thread From: Tatsuo Ishii @ 2023-09-22 04:53 UTC (permalink / raw) --- src/backend/parser/parse_agg.c | 7 + src/backend/parser/parse_clause.c | 293 +++++++++++++++++++++++++++++- src/backend/parser/parse_expr.c | 4 + src/backend/parser/parse_func.c | 3 + 4 files changed, 306 insertions(+), 1 deletion(-) diff --git a/src/backend/parser/parse_agg.c b/src/backend/parser/parse_agg.c index 85cd47b7ae..aa7a1cee80 100644 --- a/src/backend/parser/parse_agg.c +++ b/src/backend/parser/parse_agg.c @@ -564,6 +564,10 @@ check_agglevels_and_constraints(ParseState *pstate, Node *expr) errkind = true; break; + case EXPR_KIND_RPR_DEFINE: + errkind = true; + break; + /* * There is intentionally no default: case here, so that the * compiler will warn if we add a new ParseExprKind without @@ -953,6 +957,9 @@ transformWindowFuncCall(ParseState *pstate, WindowFunc *wfunc, case EXPR_KIND_CYCLE_MARK: errkind = true; break; + case EXPR_KIND_RPR_DEFINE: + errkind = true; + break; /* * There is intentionally no default: case here, so that the diff --git a/src/backend/parser/parse_clause.c b/src/backend/parser/parse_clause.c index 334b9b42bd..293d4b1680 100644 --- a/src/backend/parser/parse_clause.c +++ b/src/backend/parser/parse_clause.c @@ -100,7 +100,10 @@ static WindowClause *findWindowClause(List *wclist, const char *name); static Node *transformFrameOffset(ParseState *pstate, int frameOptions, Oid rangeopfamily, Oid rangeopcintype, Oid *inRangeFunc, Node *clause); - +static void transformRPR(ParseState *pstate, WindowClause *wc, WindowDef *windef, List **targetlist); +static List *transformDefineClause(ParseState *pstate, WindowClause *wc, WindowDef *windef, List **targetlist); +static void transformPatternClause(ParseState *pstate, WindowClause *wc, WindowDef *windef); +static List *transformMeasureClause(ParseState *pstate, WindowClause *wc, WindowDef *windef); /* * transformFromClause - @@ -2950,6 +2953,10 @@ transformWindowDefinitions(ParseState *pstate, rangeopfamily, rangeopcintype, &wc->endInRangeFunc, windef->endOffset); + + /* Process Row Pattern Recognition related clauses */ + transformRPR(pstate, wc, windef, targetlist); + wc->runCondition = NIL; wc->winref = winref; @@ -3815,3 +3822,287 @@ transformFrameOffset(ParseState *pstate, int frameOptions, return node; } + +/* + * transformRPR + * Process Row Pattern Recognition related clauses + */ +static void +transformRPR(ParseState *pstate, WindowClause *wc, WindowDef *windef, List **targetlist) +{ + /* + * Window definition exists? + */ + if (windef == NULL) + return; + + /* + * Row Pattern Common Syntax clause exists? + */ + if (windef->rpCommonSyntax == NULL) + return; + + /* Check Frame option. Frame must start at current row */ + if ((wc->frameOptions & FRAMEOPTION_START_CURRENT_ROW) == 0) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("FRAME must start at current row when row patttern recognition is used"))); + + /* Transform AFTER MACH SKIP TO clause */ + wc->rpSkipTo = windef->rpCommonSyntax->rpSkipTo; + + /* Transform SEEK or INITIAL clause */ + wc->initial = windef->rpCommonSyntax->initial; + + /* Transform DEFINE clause into list of TargetEntry's */ + wc->defineClause = transformDefineClause(pstate, wc, windef, targetlist); + + /* Check PATTERN clause and copy to patternClause */ + transformPatternClause(pstate, wc, windef); + + /* Transform MEASURE clause */ + transformMeasureClause(pstate, wc, windef); +} + +/* + * transformDefineClause Process DEFINE clause and transform ResTarget into + * list of TargetEntry. + * + * XXX we only support column reference in row pattern definition search + * condition, e.g. "price". <row pattern definition variable name>.<column + * reference> is not supported, e.g. "A.price". + */ +static List * +transformDefineClause(ParseState *pstate, WindowClause *wc, WindowDef *windef, List **targetlist) +{ + /* DEFINE variable name initials */ + static char *defineVariableInitials = "abcdefghijklmnopqrstuvwxyz"; + + ListCell *lc, *l; + ResTarget *restarget, *r; + List *restargets; + char *name; + int initialLen; + int i; + + /* + * If Row Definition Common Syntax exists, DEFINE clause must exist. + * (the raw parser should have already checked it.) + */ + Assert(windef->rpCommonSyntax->rpDefs != NULL); + + /* + * Check and add "A AS A IS TRUE" if pattern variable is missing in DEFINE + * per the SQL standard. + */ + restargets = NIL; + foreach(lc, windef->rpCommonSyntax->rpPatterns) + { + A_Expr *a; + bool found = false; + + if (!IsA(lfirst(lc), A_Expr)) + ereport(ERROR, + errmsg("node type is not A_Expr")); + + a = (A_Expr *)lfirst(lc); + name = strVal(a->lexpr); + + foreach(l, windef->rpCommonSyntax->rpDefs) + { + restarget = (ResTarget *)lfirst(l); + + if (!strcmp(restarget->name, name)) + { + found = true; + break; + } + } + + if (!found) + { + /* + * "name" is missing. So create "name AS name IS TRUE" ResTarget + * node and add it to the temporary list. + */ + A_Const *n; + + restarget = makeNode(ResTarget); + n = makeNode(A_Const); + n->val.boolval.type = T_Boolean; + n->val.boolval.boolval = true; + n->location = -1; + restarget->name = pstrdup(name); + restarget->indirection = NIL; + restarget->val = (Node *)n; + restarget->location = -1; + restargets = lappend((List *)restargets, restarget); + } + } + + if (list_length(restargets) >= 1) + { + /* add missing DEFINEs */ + windef->rpCommonSyntax->rpDefs = list_concat(windef->rpCommonSyntax->rpDefs, + restargets); + list_free(restargets); + } + + /* + * Check for duplicate row pattern definition variables. The standard + * requires that no two row pattern definition variable names shall be + * equivalent. + */ + restargets = NIL; + foreach(lc, windef->rpCommonSyntax->rpDefs) + { + restarget = (ResTarget *)lfirst(lc); + name = restarget->name; + + /* + * Add DEFINE expression (Restarget->val) to the targetlist as a + * TargetEntry if it does not exist yet. Planner will add the column + * ref var node to the outer plan's target list later on. This makes + * DEFINE expression could access the outer tuple while evaluating + * PATTERN. + * + * XXX: adding whole expressions of DEFINE to the plan.targetlist is + * not so good, because it's not necessary to evalute the expression + * in the target list while running the plan. We should extract the + * var nodes only then add them to the plan.targetlist. + */ + findTargetlistEntrySQL99(pstate, (Node *)restarget->val, targetlist, EXPR_KIND_RPR_DEFINE); + + /* + * Make sure that the row pattern definition search condition is a + * boolean expression. + */ + transformWhereClause(pstate, restarget->val, + EXPR_KIND_RPR_DEFINE, "DEFINE"); + + foreach(l, restargets) + { + char *n; + + r = (ResTarget *) lfirst(l); + n = r->name; + + if (!strcmp(n, name)) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("row pattern definition variable name \"%s\" appears more than once in DEFINE clause", + name), + parser_errposition(pstate, exprLocation((Node *)r)))); + } + restargets = lappend(restargets, restarget); + } + list_free(restargets); + + /* + * Create list of row pattern DEFINE variable name's initial. + * We assign [a-z] to them (up to 26 variable names are allowed). + */ + restargets = NIL; + i = 0; + initialLen = strlen(defineVariableInitials); + + foreach(lc, windef->rpCommonSyntax->rpDefs) + { + char initial[2]; + + restarget = (ResTarget *)lfirst(lc); + name = restarget->name; + + if (i >= initialLen) + { + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("number of row pattern definition variable names exceeds %d", initialLen), + parser_errposition(pstate, exprLocation((Node *)restarget)))); + } + initial[0] = defineVariableInitials[i++]; + initial[1] = '\0'; + wc->defineInitial = lappend(wc->defineInitial, makeString(pstrdup(initial))); + } + + return transformTargetList(pstate, windef->rpCommonSyntax->rpDefs, + EXPR_KIND_RPR_DEFINE); +} + +/* + * transformPatternClause + * Process PATTERN clause and return PATTERN clause in the raw parse tree + */ +static void +transformPatternClause(ParseState *pstate, WindowClause *wc, WindowDef *windef) +{ + ListCell *lc, *l; + + /* + * Row Pattern Common Syntax clause exists? + */ + if (windef->rpCommonSyntax == NULL) + return; + + /* + * Primary row pattern variable names in PATTERN clause must appear in + * DEFINE clause as row pattern definition variable names. + */ + wc->patternVariable = NIL; + wc->patternRegexp = NIL; + foreach(lc, windef->rpCommonSyntax->rpPatterns) + { + A_Expr *a; + char *name; + char *regexp; + bool found = false; + + if (!IsA(lfirst(lc), A_Expr)) + ereport(ERROR, + errmsg("node type is not A_Expr")); + + a = (A_Expr *)lfirst(lc); + name = strVal(a->lexpr); + + foreach(l, windef->rpCommonSyntax->rpDefs) + { + ResTarget *restarget = (ResTarget *)lfirst(l); + + if (!strcmp(restarget->name, name)) + { + found = true; + break; + } + } + + if (!found) + { + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("primary row pattern variable name \"%s\" does not appear in DEFINE clause", + name), + parser_errposition(pstate, exprLocation((Node *)a)))); + } + wc->patternVariable = lappend(wc->patternVariable, makeString(pstrdup(name))); + regexp = strVal(lfirst(list_head(a->name))); + wc->patternRegexp = lappend(wc->patternRegexp, makeString(pstrdup(regexp))); + } +} + +/* + * transformMeasureClause + * Process MEASURE clause + * XXX MEASURE clause is not supported yet + */ +static List * +transformMeasureClause(ParseState *pstate, WindowClause *wc, WindowDef *windef) +{ + if (windef->rowPatternMeasures == NIL) + return NIL; + + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("%s","MEASURE clause is not supported yet"), + parser_errposition(pstate, exprLocation((Node *)windef->rowPatternMeasures)))); + return NIL; +} diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c index 64c582c344..18b58ac263 100644 --- a/src/backend/parser/parse_expr.c +++ b/src/backend/parser/parse_expr.c @@ -557,6 +557,7 @@ transformColumnRef(ParseState *pstate, ColumnRef *cref) case EXPR_KIND_COPY_WHERE: case EXPR_KIND_GENERATED_COLUMN: case EXPR_KIND_CYCLE_MARK: + case EXPR_KIND_RPR_DEFINE: /* okay */ break; @@ -1770,6 +1771,7 @@ transformSubLink(ParseState *pstate, SubLink *sublink) case EXPR_KIND_VALUES: case EXPR_KIND_VALUES_SINGLE: case EXPR_KIND_CYCLE_MARK: + case EXPR_KIND_RPR_DEFINE: /* okay */ break; case EXPR_KIND_CHECK_CONSTRAINT: @@ -3149,6 +3151,8 @@ ParseExprKindName(ParseExprKind exprKind) return "GENERATED AS"; case EXPR_KIND_CYCLE_MARK: return "CYCLE"; + case EXPR_KIND_RPR_DEFINE: + return "DEFINE"; /* * There is intentionally no default: case here, so that the diff --git a/src/backend/parser/parse_func.c b/src/backend/parser/parse_func.c index b3f0b6a137..2ff3699538 100644 --- a/src/backend/parser/parse_func.c +++ b/src/backend/parser/parse_func.c @@ -2656,6 +2656,9 @@ check_srf_call_placement(ParseState *pstate, Node *last_srf, int location) case EXPR_KIND_CYCLE_MARK: errkind = true; break; + case EXPR_KIND_RPR_DEFINE: + errkind = true; + break; /* * There is intentionally no default: case here, so that the -- 2.25.1 ----Next_Part(Fri_Sep_22_14_16_40_2023_530)-- Content-Type: Text/X-Patch; charset=us-ascii Content-Transfer-Encoding: 7bit Content-Disposition: inline; filename="v7-0003-Row-pattern-recognition-patch-planner.patch" ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
* [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. @ 2026-06-22 07:34 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 330+ messages in thread From: Antonin Houska @ 2026-06-22 07:34 UTC (permalink / raw) The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=-- ^ permalink raw reply [nested|flat] 330+ messages in thread
end of thread, other threads:[~2026-06-22 07:34 UTC | newest] Thread overview: 330+ messages (download: mbox mbox.gz follow: Atom feed) -- links below jump to the message on this page -- 2023-09-22 04:53 [PATCH v7 2/7] Row pattern recognition patch (parse/analysis). Tatsuo Ishii <ishii@postgresql.org> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at> 2026-06-22 07:34 [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. Antonin Houska <ah@cybertec.at>
This inbox is served by agora; see mirroring instructions for how to clone and mirror all data and code used for this inbox