Received: from malur.postgresql.org ([217.196.149.56]) by arkaria.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (Exim 4.96) (envelope-from ) id 1wbcZd-002iEI-2l for pgsql-hackers@arkaria.postgresql.org; Mon, 22 Jun 2026 11:12:18 +0000 Received: from localhost ([127.0.0.1] helo=malur.postgresql.org) by malur.postgresql.org with esmtp (Exim 4.96) (envelope-from ) id 1wbcZc-006Hbu-2M for pgsql-hackers@arkaria.postgresql.org; Mon, 22 Jun 2026 11:12:16 +0000 Received: from magus.postgresql.org ([2a02:c0:301:0:ffff::29]) by malur.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (Exim 4.96) (envelope-from ) id 1wbcZc-006Hbi-1C for pgsql-hackers@lists.postgresql.org; Mon, 22 Jun 2026 11:12:16 +0000 Received: from mail-wm1-x335.google.com ([2a00:1450:4864:20::335]) by magus.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 (Exim 4.98.2) (envelope-from ) id 1wbcZZ-00000001kGM-3AJn for pgsql-hackers@lists.postgresql.org; Mon, 22 Jun 2026 11:12:15 +0000 Received: by mail-wm1-x335.google.com with SMTP id 5b1f17b1804b1-490b1bbcf3aso27419405e9.1 for ; Mon, 22 Jun 2026 04:12:13 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=cybertec.at; s=google; t=1782126732; x=1782731532; darn=lists.postgresql.org; h=message-id:date:mime-version:comments:references:in-reply-to :subject:cc:to:from:from:to:cc:subject:date:message-id:reply-to; bh=goVPCfh+NSvGpCyuvWO8Yb+V5AWM3taTCjVBUO2JzRo=; b=GGf+/jNCtV/hU0sCUiXthZcw5iLH5HUDf/76FcERSrn8mXMA8v3HiWdfH43dG1RRQ2 n4W8YpLTF/9UotCK3C/YGjRgvUAADUWEm9fkMrKsIKXo6r5v8ypZpAMUXFtWuohPI2Rr SI910lepE56ZHAfIrpPG2Qqxc2HdC1q5DYysIgQ3cQl9bXzbp7DWBg2kgJfNFR2oRl38 GFWQpokFWtc+2yRMcwgOlID6t8nevxjvJpAm4XTnwaKNoA0rc5tzSRDB55dQImdEAdE+ yUraHskoiXJQmB0jaj5WDCsnfcutB+JyZPP+dwktTzw48BgGmCH6C5/AJuWNy15BED3p hQhw== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20251104; t=1782126732; x=1782731532; h=message-id:date:mime-version:comments:references:in-reply-to :subject:cc:to:from:x-gm-gg:x-gm-message-state:from:to:cc:subject :date:message-id:reply-to; bh=goVPCfh+NSvGpCyuvWO8Yb+V5AWM3taTCjVBUO2JzRo=; b=KHYRjI8dKOA9AKGrtP4Hvnc5YccIxklXi9CST7reIoKrw+KJrE7UwDL3gjnuPYlyfh K+HQvf3y4TFV72k9gN+ALkJywlfQ/zo2v01hSK/CgMi+j8Qx8yYrQuJKxra+qgRPxqFt wGJ/lHyTCl+LX2ENy5ZCei30x4rvGfIStLmCrUSgAv+BbremQEXrZT/R3s+dwG2rVDg2 o8nWQd5QmSh8gOmuJceFnDB0anywDDXdAq4RBTbrjClfaq7zsWlVEuOMs5kaLm5E7ted CHIkotgbaFikewFtIUgjLRSonyAWifM9IRwRyafCp1R9hNKG5lOqqEJXzqoKuhX5IkTn 6yFw== X-Gm-Message-State: AOJu0YyH5kQgMNLuv779boLwwSX3tGAlruW7Qi3i3q0P4y8DTnpTsCZb X9i/yPT0x+KKianovR1FlTNjovR/oZqSOr3SiMGTWngyWbC4ea+lI8MV/HGIq32k0qY= X-Gm-Gg: AfdE7cmeGkm91kpmf6Ruc7sOa2sScNw3QoWGP0CP0NSZqHFoyJAtYAyAhZzD66FcrtZ 55k7p/klN1r/PruX8/fWUjQR9g9iNdG9SxA11HG7tOUrPGuEc0ksU5hSOeWYh3gslh4Imbfyd8C 2pfurGWar0nZ20ryVDd1t4AYIVoY1Nu0M8gzxulSSy0Vm+ABAFjdrUkUTzU2oLqic2NAUnk+iCB dcFtibY7asyylxaT3pPknZkCZnFo4Md6wvNitz+FY1rG/gUeIAnH8XWpyuueBNtKGswQPWhJqz1 mYRfYz3XJag4KeRTDsM48lj5XaeKL+tovT27vwp8qEKTcN8tCXhv4X4uyytX1INXiS5DxTzXuOK Nrrnuyj6vwoSGGpAa3Ice5/7mPo6v1Tnf71uk9wxr1Ad5BwX0RKZYc0VvQvCEnQtHnFRELv6us8 U9yA/36dgYf461qgNZAEczpaMDzA== X-Received: by 2002:a05:600c:154a:b0:492:3e66:6c84 with SMTP id 5b1f17b1804b1-492490af5c2mr150711605e9.30.1782126732530; Mon, 22 Jun 2026 04:12:12 -0700 (PDT) Received: from localhost (109-81-168-148.rct.o2.cz. [109.81.168.148]) by smtp.gmail.com with ESMTPSA id 5b1f17b1804b1-492494497ffsm212254925e9.11.2026.06.22.04.12.11 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Mon, 22 Jun 2026 04:12:12 -0700 (PDT) From: Antonin Houska To: Ewan Young cc: PostgreSQL Hackers , mihailnikalayeu@gmail.com, alvherre@kurilemu.de Subject: Re: REPACK CONCURRENTLY fails on tables with generated columns In-reply-to: References: Comments: In-reply-to Ewan Young message dated "Fri, 12 Jun 2026 16:39:54 +0800." X-Mailer: MH-E 8.6+git; nmh 1.8; GNU Emacs 28.3 MIME-Version: 1.0 Content-Type: multipart/mixed; boundary="=-=-=" Date: Mon, 22 Jun 2026 13:12:11 +0200 Message-ID: <18222.1782126731@localhost> List-Id: List-Help: List-Subscribe: List-Post: List-Owner: List-Archive: Archived-At: Precedence: bulk --=-=-= Content-Type: text/plain Ewan Young wrote: > The transient heap built by make_new_heap() is intentionally created > without the old table's defaults and constraints, so it has no generation > expressions for its generated columns, even though the tuple descriptor > still has attgenerated set. > > When apply_concurrent_update() replays a non-HOT update, it calls > ExecInsertIndexTuples() with EIIT_IS_UPDATE. To decide whether to pass > the "indexUnchanged" hint, that calls index_unchanged_by_update() -> > ExecGetExtraUpdatedCols() -> ExecInitGenerated(), which looks up the > generation expression of each generated column via build_column_default() > and errors out when it finds none on the transient heap. > > The apply path does not need to recompute generated columns at all: the > decoded tuple already carries the correct value, and it is only inserted. > Note also that ExecGetUpdatedCols() already returns an empty set for this > ResultRelInfo, because it is not part of any range table -- so the > indexUnchanged determination here is already approximate. I'm sorry for the confusion, but the fact that ExecGetUpdatedCols() returns an empty set is an omission rather than deliberate choice. Assuming we fix that, the result of ExecGetExtraUpdatedCols() does matter. Thus we should copy the related pg_attrdef entries, as I suggest in this patch. Another question is how serious problem it is that ExecGetUpdatedCols() returns empty set. AFAICS, "indexUnchanged" does not affect correctness - it's is only a hint that helps the btree AM decide whent the bottom-up deletion and de-duplication techniques should (not) be used. I'm not sure it's easy to update the set for individual UPDATEs: the UPDATE commands REPACK replays originate from different SQL queries and the logical decoding does not transfer this information. Even then, I think it'd be "less bad" to have ExecGetUpdatedCols() return a set containing all the attributes rather than empty set. That is, avoid using the btree optimizations altogether rather than allow them them when not appropriate. However, per index_unchanged_by_update(), if ExecGetUpdatedCols() tells that all columns are updated, the result of ExecGetExtraUpdatedCols() does not matter. Nevertheless, I'd still slightly prefer copying the pg_attrdef entries to hacking the executor. -- Antonin Houska Web: https://www.cybertec-postgresql.com --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=0001-Copy-the-relevant-pg_attrdef-catalog-entries-for-the.patch From b6ae449d32c2b2ce7ec12605effc32b579497c2f Mon Sep 17 00:00:00 2001 From: Antonin Houska Date: Mon, 22 Jun 2026 09:34:05 +0200 Subject: [PATCH] Copy the relevant pg_attrdef catalog entries for the transient relation. The default values may be needed by the executor when processing the concurrent data changes. In particular, ExecInsertIndexTuples() needs it when determining the value of the 'indexUnchanged' hint for the index AM. Like in copy_index_constraints(), we make the new catalog entries dependent on the transient relation, so they are dropped along with it automatically. --- src/backend/commands/repack.c | 97 +++++++++++++++++++ .../injection_points/specs/repack.spec | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 4d177c868bb..725efa236bc 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -48,6 +48,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -204,6 +205,7 @@ static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHea static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static void copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id); +static void copy_attribute_defaults(Relation old_heap, Relation new_heap); static Relation process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, @@ -1083,6 +1085,13 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); NewHeap = table_open(OIDNewHeap, NoLock); + /* + * Copy attribute defaults - the executor may need them, in order to + * process the concurrent data changes. In particular, this is related to + * ExecInsertIndexTuples(). + */ + copy_attribute_defaults(OldHeap, NewHeap); + /* Copy the heap data into the new table in the desired order */ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -3434,6 +3443,94 @@ copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) CommandCounterIncrement(); } +/* + * Create a transient copy of attribute defaults for the transient table. + * + * Like above, the executor needs information on attribute defaults. Once the + * repacking is finished, the catalog entries we create here are dropped. + */ +static void +copy_attribute_defaults(Relation old_heap, Relation new_heap) +{ + Oid old_heap_id = RelationGetRelid(old_heap); + Oid new_heap_id = RelationGetRelid(new_heap); + ScanKeyData skey; + Relation rel; + Relation att_rel = NULL; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(AttrDefaultRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + ScanKeyInit(&skey, + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_heap_id)); + scan = systable_beginscan(rel, AttrDefaultIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_attrdef adform; + Oid oid; + Datum values[Natts_pg_attrdef] = {0}; + bool nulls[Natts_pg_attrdef] = {0}; + bool replaces[Natts_pg_attrdef] = {0}; + HeapTuple new_tup, att_tup, att_new_tup; + ObjectAddress objad; + Datum att_values[Natts_pg_attribute] = {0}; + bool att_nulls[Natts_pg_attribute] = {0}; + bool att_replaces[Natts_pg_attribute] = {0}; + + adform = (Form_pg_attrdef) GETSTRUCT(tup); + Assert(adform->adrelid == old_heap_id); + + oid = GetNewOidWithIndex(rel, AttrDefaultOidIndexId, + Anum_pg_attrdef_oid); + values[Anum_pg_attrdef_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_attrdef_oid - 1] = true; + values[Anum_pg_attrdef_adrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_attrdef_adrelid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objad, AttrDefaultRelationId, oid); + recordDependencyOn(&objad, &objrel, DEPENDENCY_AUTO); + + /* Set atthasdef - new heap has it cleared. */ + att_tup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(new_heap_id), + ObjectIdGetDatum(adform->adnum)); + if (!HeapTupleIsValid(att_tup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + adform->adnum, new_heap_id); + + att_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(true); + att_replaces[Anum_pg_attribute_atthasdef - 1] = true; + + if (att_rel == NULL) + att_rel = table_open(AttributeRelationId, RowExclusiveLock); + att_new_tup = heap_modify_tuple(att_tup, RelationGetDescr(att_rel), + att_values, att_nulls, att_replaces); + ReleaseSysCache(att_tup); + CatalogTupleUpdate(att_rel, &att_new_tup->t_self, att_new_tup); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + if (att_rel) + table_close(att_rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + /* * Try to start a background worker to perform logical decoding of data * changes applied to relation while REPACK CONCURRENTLY is copying its diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index d727a9b056b..7896d1456ad 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -3,7 +3,8 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE repack_test(i int PRIMARY KEY, j int, + k int GENERATED ALWAYS AS (j * 2) STORED); INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); CREATE TABLE relfilenodes(node oid); -- 2.52.0 --=-=-=--