agora inbox for pgsql-hackers@postgresql.org  
help / color / mirror / Atom feed
[PATCH v4 2/3] Remove HAS_TEST_AND_SET.
329+ messages / 2 participants
[nested] [flat]

* [PATCH v4 2/3] Remove HAS_TEST_AND_SET.
@ 2026-05-04 21:24 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 329+ messages in thread

From: Nathan Bossart @ 2026-05-04 21:24 UTC (permalink / raw)

This is only set when TAS is defined, so we can just check whether
TAS is defined directly instead.
---
 src/include/storage/s_lock.h | 20 +++++---------------
 1 file changed, 5 insertions(+), 15 deletions(-)

diff --git a/src/include/storage/s_lock.h b/src/include/storage/s_lock.h
index dcfec8ce2af..fb872edd2f0 100644
--- a/src/include/storage/s_lock.h
+++ b/src/include/storage/s_lock.h
@@ -124,7 +124,6 @@
 
 
 #ifdef __i386__		/* 32-bit i386 */
-#define HAS_TEST_AND_SET
 
 typedef unsigned char slock_t;
 
@@ -194,7 +193,6 @@ spin_delay(void)
 
 
 #ifdef __x86_64__		/* AMD Opteron, Intel EM64T */
-#define HAS_TEST_AND_SET
 
 typedef unsigned char slock_t;
 
@@ -249,7 +247,6 @@ spin_delay(void)
  */
 #if defined(__arm__) || defined(__arm) || defined(__aarch64__)
 #ifdef HAVE_GCC__SYNC_INT32_TAS
-#define HAS_TEST_AND_SET
 
 #define TAS(lock) tas(lock)
 
@@ -292,7 +289,6 @@ spin_delay(void)
 
 /* S/390 and S/390x Linux (32- and 64-bit zSeries) */
 #if defined(__s390__) || defined(__s390x__)
-#define HAS_TEST_AND_SET
 
 typedef unsigned int slock_t;
 
@@ -321,7 +317,6 @@ tas(volatile slock_t *lock)
  * acquire/release semantics. The CPU will treat superfluous members as
  * NOPs, so it's just code space.
  */
-#define HAS_TEST_AND_SET
 
 typedef unsigned char slock_t;
 
@@ -392,7 +387,6 @@ do \
 
 /* PowerPC */
 #if defined(__ppc__) || defined(__powerpc__) || defined(__ppc64__) || defined(__powerpc64__)
-#define HAS_TEST_AND_SET
 
 typedef unsigned int slock_t;
 
@@ -453,7 +447,6 @@ do \
 
 
 #if defined(__mips__) && !defined(__sgi)	/* non-SGI MIPS */
-#define HAS_TEST_AND_SET
 
 typedef unsigned int slock_t;
 
@@ -531,10 +524,9 @@ do \
  * grounds that that's known to be more likely to work in the ARM ecosystem.
  * (But we dealt with ARM above.)
  */
-#if !defined(HAS_TEST_AND_SET)
+#if !defined(TAS)
 
 #if defined(HAVE_GCC__SYNC_INT32_TAS)
-#define HAS_TEST_AND_SET
 
 #define TAS(lock) tas(lock)
 
@@ -549,7 +541,6 @@ tas(volatile slock_t *lock)
 #define S_UNLOCK(lock) __sync_lock_release(lock)
 
 #elif defined(HAVE_GCC__SYNC_CHAR_TAS)
-#define HAS_TEST_AND_SET
 
 #define TAS(lock) tas(lock)
 
@@ -565,7 +556,7 @@ tas(volatile slock_t *lock)
 
 #endif	 /* HAVE_GCC__SYNC_INT32_TAS */
 
-#endif	/* !defined(HAS_TEST_AND_SET) */
+#endif	/* !defined(TAS) */
 
 
 /*
@@ -592,12 +583,11 @@ tas(volatile slock_t *lock)
  * ---------------------------------------------------------------------
  */
 
-#if !defined(HAS_TEST_AND_SET)	/* We didn't trigger above, let's try here */
+#if !defined(TAS)	/* We didn't trigger above, let's try here */
 
 #ifdef _MSC_VER
 typedef LONG slock_t;
 
-#define HAS_TEST_AND_SET
 #define TAS(lock) (InterlockedCompareExchange(lock, 1, 0))
 
 #define SPIN_DELAY() spin_delay()
@@ -649,11 +639,11 @@ spin_delay(void)
 #endif
 
 
-#endif	/* !defined(HAS_TEST_AND_SET) */
+#endif	/* !defined(TAS) */
 
 
 /* Blow up if we didn't have any way to do spinlocks */
-#ifndef HAS_TEST_AND_SET
+#ifndef TAS
 #error PostgreSQL does not have spinlock support on this platform.  Please report this to pgsql-bugs@lists.postgresql.org.
 #endif
 
-- 
2.50.1 (Apple Git-155)


--JmXY/Yn6swRVDyih
Content-Type: text/plain; charset=us-ascii
Content-Disposition: attachment;
	filename=v4-0003-Better-express-platform-requirements-in-s_lock.h.patch



^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread

* [PATCH 2/2] Provide the executor with information on updated columns.
@ 2026-07-01 13:18 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 329+ messages in thread

From: Antonin Houska @ 2026-07-01 13:18 UTC (permalink / raw)

When replaying data changes, REPACK calls ExecInsertIndexTuples() for each
INSERT and UPDATE. In the latter case, the function needs to determine the
value of the 'indexUnchanged' hint for the index AM. (The hint is always false
for INSERT.) Therefore, REPACK is supposed to specify which columns are
changed by the UPDATE.

So far, REPACK missed to specify the set of updated columns, so the
'indexUnchanged' hint could incorrectly evaluate to true. Although it should
not affect correctness, it can make the index AM use optimizations that are
not appropriate.  Ideally, we should compute the set of updated columns for
each individual UPDATE, however the comparison of the old and new tuple might
add too much overhead.

This patch initializes the set as if all columns were updated. Thus
'indexUnchanged' always evaluates to false. This way the index AM never uses
the related optimizations. It might result in worse structure of the index,
however it seems better to not use the optimizations than to misuse them.
---
 src/backend/commands/repack.c | 40 ++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index c9b0c047477..f37bb9a21af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -62,6 +62,7 @@
 #include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "replication/logicalrelation.h"
 #include "storage/bufmgr.h"
@@ -3005,13 +3006,50 @@ static void
 initialize_change_context(ChangeContext *chgcxt,
 						  Relation relation, Oid ident_index_id)
 {
+	Bitmapset	*updatedCols = NULL;
+	RangeTblEntry *rte;
+	List	   *perminfos = NIL;
+	RTEPermissionInfo *perminfo;
+
 	chgcxt->cc_rel = relation;
 
 	/* Only initialize fields needed by ExecInsertIndexTuples(). */
 	chgcxt->cc_estate = CreateExecutorState();
 
+	/*
+	 * Initialize updatedCols.
+	 *
+	 * The point is that ExecInsertIndexTuples() should not pass the
+	 * indexUnchanged hint to the index AM unless there's a reason to do
+	 * so. For simplicity, we consider all columns updated.
+	 *
+	 * XXX Should we spend more effort to compare the old and new tuple when
+	 * replaying UPDATE, or at least exclude unchanged TOAST values, like we
+	 * do in logicalrep_write_tuple()?
+	 */
+	for (int i = 0; i < RelationGetDescr(relation)->natts; i++)
+		updatedCols = bms_add_member(updatedCols,
+									 i + 1 - FirstLowInvalidHeapAttributeNumber);
+
+	/*
+	 * In this case, RTE only needs to have ->perminfoindex initialized, but
+	 * there's no reason to not set the fields whose values we have at hand.
+	 */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(relation);
+	rte->relkind = RelationGetForm(relation)->relkind;
+	/* Create the RTEPermissionInfo instance (and set ->perminfoindex). */
+	addRTEPermissionInfo(&perminfos, rte);
+	/* Make updatedCols available to the executor functions. */
+	perminfo = getRTEPermissionInfo(perminfos, rte);
+	perminfo->updatedCols = updatedCols;
+
+	ExecInitRangeTable(chgcxt->cc_estate, list_make1(rte), perminfos,
+					   bms_make_singleton(1));
+
 	chgcxt->cc_rri = makeNode(ResultRelInfo);
-	InitResultRelInfo(chgcxt->cc_rri, relation, 0, NULL, 0);
+	InitResultRelInfo(chgcxt->cc_rri, relation, 1, NULL, 0);
 	ExecOpenIndices(chgcxt->cc_rri, false);
 
 	/*
-- 
2.52.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 329+ messages in thread


end of thread, other threads:[~2026-07-01 13:18 UTC | newest]

Thread overview: 329+ messages (download: mbox mbox.gz follow: Atom feed)
-- links below jump to the message on this page --
2026-05-04 21:24 [PATCH v4 2/3] Remove HAS_TEST_AND_SET. Nathan Bossart <nathan@postgresql.org>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. Antonin Houska <ah@cybertec.at>
2026-07-01 13:18 [PATCH 2/2] Provide the executor with information on updated columns. 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