Received: from malur.postgresql.org ([217.196.149.56]) by arkaria.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (Exim 4.96) (envelope-from ) id 1wd0KG-003ob5-24 for pgsql-hackers@arkaria.postgresql.org; Fri, 26 Jun 2026 06:46:08 +0000 Received: from localhost ([127.0.0.1] helo=malur.postgresql.org) by malur.postgresql.org with esmtp (Exim 4.96) (envelope-from ) id 1wd0KF-009a3O-1l for pgsql-hackers@arkaria.postgresql.org; Fri, 26 Jun 2026 06:46:07 +0000 Received: from magus.postgresql.org ([2a02:c0:301:0:ffff::29]) by malur.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (Exim 4.96) (envelope-from ) id 1wd0KF-009a29-0j for pgsql-hackers@lists.postgresql.org; Fri, 26 Jun 2026 06:46:07 +0000 Received: from mail-pl1-x62b.google.com ([2607:f8b0:4864:20::62b]) by magus.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 (Exim 4.98.2) (envelope-from ) id 1wd0KD-00000000JxE-1682 for pgsql-hackers@lists.postgresql.org; Fri, 26 Jun 2026 06:46:06 +0000 Received: by mail-pl1-x62b.google.com with SMTP id d9443c01a7336-2c7f3148705so5492005ad.1 for ; Thu, 25 Jun 2026 23:46:05 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20251104; t=1782456363; x=1783061163; darn=lists.postgresql.org; h=content-disposition:mime-version:message-id:subject:cc:to:from:date :from:to:cc:subject:date:message-id:reply-to; bh=nJvxDqCaZGKAGslD6LWLKVMZrzRpuDWrxLFBLhJrPDI=; b=c0VJ1x9Vcuo9G1HXFn8V9CZFBgEzzLYcqdkXXi9gKf1lgfMKrqQRiHVpceS0Eqf4o7 cD8j/NATZk9mzroDweE+PxLD9yYu0lKI9+ZvvTVM2xinUqtXSHCdLP8wvtTTag4yhUyW JGeBBRI6wbJ4Ikdvoord6E8BJ/+vpAJCnGZ4EXSj1Eq/NwXKu077dkxeFBkbZmOKeKmb dVai2Qf+m4bgmunNkUsgyWPBVqXwWkvHyE6/z5IyJ+LVZ+SzKD9jcznupwX2hICHqaXu fC6JguAVMm3WSuvcZH0V/EqfstRFh2lIU6LNCsC48JAXQf98ayhjkLLkmQmvG26CSowR ng+w== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20251104; t=1782456363; x=1783061163; h=content-disposition:mime-version:message-id:subject:cc:to:from:date :x-gm-gg:x-gm-message-state:from:to:cc:subject:date:message-id :reply-to; bh=nJvxDqCaZGKAGslD6LWLKVMZrzRpuDWrxLFBLhJrPDI=; b=ZuNZluCTlxTJ2y7Ze/yD74SruxRq8GNeLgAgXQQPtw7qWJlQkuEoJhuwYze0bHUZ7f 0flihvkk3/XBk0IDdZ9RopdN9iKV/rBIcs0kTqrid9yaNgB7AIlQShuOUwXMAm0PiwK4 Wswu2CpxLDfw47ofLdl96gW+47frq8LFl5Lnw0toO5i1MiWYowm0zsKJDrmlR3vAwOv/ S0hXT4YmIoLPsPE4ZtFFKX6WU3+yutlEJM97Q1oJq8XSxjjKX/6LY+gNlxZg6tL/tHHa M/9VveUQNR95RIE9P5GeckrwxkCIUDAfHpoqaVsK6cfinflUuVe/sW/9jbIOW4aZhBQ2 p2/A== X-Gm-Message-State: AOJu0YwcET1JvePZxpTu/1WJOWGI70oC8d+6c5tZvxOZXKCEydduYePT 0IvJjn6Q43JnzHxzfzFMw9M1nSHftzSSIxy7BWwgVFf9Q1KEGPPTn45hBvIWMYlgh1s= X-Gm-Gg: AfdE7ck+Dx8xcmgVQa8RNBMr7mIhJVT9SSGxCpnFNrij6Of+mRe3LlhUbEeTTi4lm7l uVAcXGeXRsPFYp+ga2F1LKOOOFzHKb4azCX8RTSILej13nFQpsDTspFsnNq03UMpvXwLEYERKRT LVOozbzheTfSU3mAG8WELwO8qUiDh14cg6dYtBQTAhDA/fTRrKbYELFHW7CIRRhz2iy/J2CK54c 6mGMtiZ5RDMFIF7MOTKCUxBs7ZHFEUbulRZoTZ++B1DlHj5U5PkAUMmzEwTTjr7ke9BZFnIWGC8 FRcDx4b0HMtebjRjPRIzft894MjT9d1tP0wDPjMquXqdJyypmnWWQ3kdNm79fILrcx/XRf6Narc UFZbaUE4NGBBqhkk0yZRZS3dYDy1ZzyeT9jX/wVwPXWraiWJpniSKIxS7gL6BKkS2jg2Ycw0XG9 N2y8Q5I09If2F1WhAxHoYWn8FodBtS/VnJ3/ZBfrjyUqRY X-Received: by 2002:a17:902:cf07:b0:2c3:5683:9acb with SMTP id d9443c01a7336-2c7fccc7b6bmr77198495ad.32.1782456363137; Thu, 25 Jun 2026 23:46:03 -0700 (PDT) Received: from localhost (103-127-218-188.dynamic-ip.pni.tw. [103.127.218.188]) by smtp.gmail.com with ESMTPSA id d9443c01a7336-2c7f5b04be8sm33501565ad.35.2026.06.25.23.46.01 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Thu, 25 Jun 2026 23:46:01 -0700 (PDT) Date: Fri, 26 Jun 2026 14:45:57 +0800 From: Adam Lee To: pgsql-hackers@lists.postgresql.org Cc: Antonin Houska , =?iso-8859-1?Q?=C1lvaro?= Herrera Subject: CLUSTER progress: wrong index_rebuild_count for tables with TOAST Message-ID: MIME-Version: 1.0 Content-Type: multipart/mixed; boundary="MhLLfZjzjFMK15NK" Content-Disposition: inline List-Id: List-Help: List-Subscribe: List-Post: List-Owner: List-Archive: Archived-At: Precedence: bulk --MhLLfZjzjFMK15NK Content-Type: text/plain; charset=us-ascii Content-Disposition: inline Hi, When I run CLUSTER (or VACUUM FULL / REPACK) on a table that has a TOAST table, pg_stat_progress_cluster shows a wrong index_rebuild_count. During the heap scan the count is already equal to the number of indexes, but it should be 0 until the indexes are rebuilt at the end. A simple way to reproduce it. Run CLUSTER in one session, and query the view from another session at the same time: ``` CREATE TABLE t (i int, x text); INSERT INTO t SELECT g, repeat(md5(g::text), 1000) FROM generate_series(1, 5) g; CREATE INDEX ON t (i); CLUSTER t USING t_i_idx; -- phase "seq scanning heap", index_rebuild_count = 2 (should be 0) ``` The reason is that make_new_heap() creates the new TOAST table, and its TOAST index, before the heap is scanned. Building that index reports CREATE INDEX progress. CREATE INDEX and the cluster command use the same progress field for different things, so the cluster command then shows a CREATE INDEX value as its index_rebuild_count. The fix is to not report progress for the TOAST index build, because it is an internal index, not a user CREATE INDEX. The patch also adds an isolation test that pauses CLUSTER at the start of the heap scan and checks that index_rebuild_count is 0. -- Adam --MhLLfZjzjFMK15NK Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v1-0001-Suppress-CREATE-INDEX-progress-while-building-a-T.patch From 5d701f806b702d1003a4ca6ec56a98cb59b4920c Mon Sep 17 00:00:00 2001 From: Adam Lee Date: Fri, 26 Jun 2026 14:31:25 +0800 Subject: [PATCH v1] Suppress CREATE INDEX progress while building a TOAST index create_toast_table() built the TOAST table's index without INDEX_CREATE_SUPPRESS_PROGRESS, so index_build() reported CREATE INDEX progress for it. By itself that is merely pointless -- a TOAST index is never the target of a user-issued CREATE INDEX -- but it is actively wrong when the TOAST table is created while another command is already reporting progress for the same backend. make_new_heap(), used by CLUSTER and VACUUM FULL, is exactly such a case: it creates the new relation's TOAST table, and thus the TOAST index, before the old heap is scanned. CREATE INDEX and REPACK share the st_progress_param[] slots -- PROGRESS_CREATEIDX_PHASE and PROGRESS_REPACK_INDEX_REBUILD_COUNT are both slot 9 -- so building the TOAST index overwrote the in-progress cluster's index_rebuild_count. pg_stat_progress_cluster therefore showed index_rebuild_count = 2 (the numeric value of PROGRESS_CREATEIDX_PHASE_BUILD) while the command was still scanning the heap, where it must read 0 until the indexes are rebuilt at the end. Pass INDEX_CREATE_SUPPRESS_PROGRESS for the TOAST index to keep the build from touching the surrounding command's progress counters. Also add an isolation test that suspends CLUSTER at the start of the heap scan, using a new injection point, and confirms index_rebuild_count is still 0 at that point. --- src/backend/access/heap/heapam_handler.c | 7 +++ src/backend/catalog/toasting.c | 14 +++++- src/test/modules/injection_points/Makefile | 1 + .../expected/cluster_progress.out | 30 +++++++++++ src/test/modules/injection_points/meson.build | 1 + .../specs/cluster_progress.spec | 50 +++++++++++++++++++ 6 files changed, 102 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/cluster_progress.out create mode 100644 src/test/modules/injection_points/specs/cluster_progress.spec diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 2268cc277bc..10c88953acb 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -45,6 +45,7 @@ #include "storage/procarray.h" #include "storage/smgr.h" #include "utils/builtins.h" +#include "utils/injection_point.h" #include "utils/rel.h" #include "utils/tuplesort.h" @@ -706,6 +707,12 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + /* + * Allow a test to observe progress reporting at this point, with the scan + * phase set but no tuples scanned yet. + */ + INJECTION_POINT("heap-cluster-scan-start", NULL); + /* * Scan through the OldHeap, either in OldIndex order or sequentially; * copy each tuple into the NewHeap, or transiently to the tuplesort diff --git a/src/backend/catalog/toasting.c b/src/backend/catalog/toasting.c index 4aa52a4bd25..d95a5ddf8c1 100644 --- a/src/backend/catalog/toasting.c +++ b/src/backend/catalog/toasting.c @@ -325,6 +325,17 @@ create_toast_table(Relation rel, Oid toastOid, Oid toastIndexOid, coloptions[0] = 0; coloptions[1] = 0; + /* + * Build the TOAST index with progress reporting suppressed. This is an + * internal index, never a user-issued CREATE INDEX, so reporting CREATE + * INDEX progress for it is meaningless. More importantly, a TOAST table + * (hence this index) can be created while another progress command is + * active for the same backend -- e.g. make_new_heap() during a + * CLUSTER/VACUUM FULL (REPACK). CREATE INDEX and REPACK share progress + * parameter slots (PROGRESS_CREATEIDX_PHASE vs + * PROGRESS_REPACK_INDEX_REBUILD_COUNT), so an unsuppressed build would + * clobber the enclosing command's progress view. + */ index_create(toast_rel, toast_idxname, toastIndexOid, InvalidOid, InvalidOid, InvalidOid, indexInfo, @@ -332,7 +343,8 @@ create_toast_table(Relation rel, Oid toastOid, Oid toastIndexOid, BTREE_AM_OID, rel->rd_rel->reltablespace, collationIds, opclassIds, NULL, coloptions, NULL, (Datum) 0, - INDEX_CREATE_IS_PRIMARY, 0, true, true, NULL); + INDEX_CREATE_IS_PRIMARY | INDEX_CREATE_SUPPRESS_PROGRESS, + 0, true, true, NULL); table_close(toast_rel, NoLock); diff --git a/src/test/modules/injection_points/Makefile b/src/test/modules/injection_points/Makefile index c01d2fb095c..69ee1230046 100644 --- a/src/test/modules/injection_points/Makefile +++ b/src/test/modules/injection_points/Makefile @@ -13,6 +13,7 @@ REGRESS = injection_points hashagg reindex_conc vacuum REGRESS_OPTS = --dlpath=$(top_builddir)/src/test/regress ISOLATION = basic \ + cluster_progress \ inplace \ repack \ repack_temporal \ diff --git a/src/test/modules/injection_points/expected/cluster_progress.out b/src/test/modules/injection_points/expected/cluster_progress.out new file mode 100644 index 00000000000..4fcf82ed402 --- /dev/null +++ b/src/test/modules/injection_points/expected/cluster_progress.out @@ -0,0 +1,30 @@ +Parsed test spec with 2 sessions + +starting permutation: s1_cluster s2_progress s2_wakeup +injection_points_attach +----------------------- + +(1 row) + +step s1_cluster: CLUSTER cluster_progress USING cluster_progress_i; +step s2_progress: + SELECT relid::regclass, phase, index_rebuild_count + FROM pg_stat_progress_cluster; + +relid |phase |index_rebuild_count +----------------+-----------------+------------------- +cluster_progress|seq scanning heap| 0 +(1 row) + +step s2_wakeup: SELECT injection_points_wakeup('heap-cluster-scan-start'); +injection_points_wakeup +----------------------- + +(1 row) + +step s1_cluster: <... completed> +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/meson.build b/src/test/modules/injection_points/meson.build index 59dba1cb023..3f4796102d1 100644 --- a/src/test/modules/injection_points/meson.build +++ b/src/test/modules/injection_points/meson.build @@ -44,6 +44,7 @@ tests += { 'isolation': { 'specs': [ 'basic', + 'cluster_progress', 'inplace', 'repack', 'repack_temporal', diff --git a/src/test/modules/injection_points/specs/cluster_progress.spec b/src/test/modules/injection_points/specs/cluster_progress.spec new file mode 100644 index 00000000000..de9ead458ba --- /dev/null +++ b/src/test/modules/injection_points/specs/cluster_progress.spec @@ -0,0 +1,50 @@ +# Verify pg_stat_progress_cluster.index_rebuild_count during the table-scan +# phase of CLUSTER. +# +# A CLUSTER (REPACK) on a table that has a TOAST relation builds the new +# table's TOAST index in make_new_heap(), before the heap is scanned. That +# internal index build must not report CREATE INDEX progress into the +# enclosing REPACK command: the two commands share progress slot 9 +# (PROGRESS_CREATEIDX_PHASE vs PROGRESS_REPACK_INDEX_REBUILD_COUNT), so an +# unsuppressed build leaves index_rebuild_count looking like a CREATE INDEX +# phase value while the cluster is still scanning the heap. It must read 0 +# until indexes are actually rebuilt at the end. + +setup +{ + CREATE EXTENSION injection_points; + + -- A table with a TOAST relation (wide, toastable column) and an index to + -- cluster on. + CREATE TABLE cluster_progress (i int, t text); + INSERT INTO cluster_progress + SELECT g, repeat(md5(g::text), 1000) FROM generate_series(1, 5) g; + CREATE INDEX cluster_progress_i ON cluster_progress (i); +} + +teardown +{ + DROP TABLE cluster_progress; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('heap-cluster-scan-start', 'wait'); +} +step s1_cluster { CLUSTER cluster_progress USING cluster_progress_i; } +teardown { SELECT injection_points_detach('heap-cluster-scan-start'); } + +session s2 +# CLUSTER is suspended at the start of the heap scan; index_rebuild_count must +# be 0 here (indexes are rebuilt only at the end). +step s2_progress +{ + SELECT relid::regclass, phase, index_rebuild_count + FROM pg_stat_progress_cluster; +} +step s2_wakeup { SELECT injection_points_wakeup('heap-cluster-scan-start'); } + +permutation s1_cluster s2_progress s2_wakeup -- 2.52.0 --MhLLfZjzjFMK15NK--