-- J4-1: pg_stat_progress_cluster invents command = 'CLUSTER' for every row the
-- caller has no privilege to see, where PG18 reported NULL.
--
-- pg_stat_progress_repack is built directly on pg_stat_get_progress_info(),
-- which hands out pid and datid to everyone and NULLs relid plus all ten
-- progress parameters unless the caller has privileges of the role running the
-- command (or of pg_read_all_stats).  All its CASE expressions are written on
-- those parameters, so a redacted row comes out with every column NULL, which
-- is right.
--
-- pg_stat_progress_cluster is no longer built on the SRF: since REPACK it is a
-- view over pg_stat_progress_repack that maps the new 'REPACK' command name
-- back onto the old two.  Its CASE tests `command IN ('CLUSTER','VACUUM FULL')`
-- and then `repack_index_relid = 0`; on a redacted row both are NULL, neither
-- WHEN is true, and the ELSE arm fires.
--
-- Expected: an onlooker with no privileges of the running role learns that
--           some rewrite is in progress in some database, and nothing else -
--           command NULL, exactly as pg_stat_progress_repack reports and
--           exactly as PG18's pg_stat_progress_cluster reported.
-- Actual:   command = 'CLUSTER' for every redacted row, whatever the command
--           really was.  Measured against a VACUUM FULL: the privileged
--           reader sees 'VACUUM FULL', the unprivileged one is told 'CLUSTER'.
--
-- Anchors (master @6c5f1d60742):
--   system_views.sql:1393-1409   CREATE VIEW pg_stat_progress_cluster, the
--                                CASE ... ELSE 'CLUSTER' END at :1399-1401
--   system_views.sql:1360-1389   pg_stat_progress_repack, every column a CASE
--                                or a cast over a param, so NULL when redacted
--   pgstatfuncs.c:390-406        pid and datid to all callers, relid and
--                                param1..param10 only under
--                                HAS_PGSTAT_PERMISSIONS(beentry->st_userid)
--   pgstatfuncs.c:39             HAS_PGSTAT_PERMISSIONS = pg_read_all_stats or
--                                has_privs_of_role(GetUserId(), st_userid)
--   repack.c:517-518             pgstat_progress_start_command(REPACK, oid)
--                                and PROGRESS_REPACK_COMMAND
--   progress.h:85-94             PROGRESS_REPACK_COMMAND = param1,
--                                PROGRESS_REPACK_INDEX_RELID = param3
--
-- REL_18_STABLE's system_views.sql:1247-1272 spells the same column as
--   CASE S.param1 WHEN 1 THEN 'CLUSTER' WHEN 2 THEN 'VACUUM FULL' END
-- straight off the SRF, so a redacted row yielded NULL there: no WHEN matches
-- a NULL param and there is no ELSE.  Introduced by ac58465e061 (2026-03-10,
-- "Introduce the REPACK command"), unreleased.  Fix is one line - make the
-- ELSE conditional on command IS NOT NULL, or map the command name with a
-- third CASE over the same NULL-preserving shape as the other views.
--
-- Two sessions.  Run the whole file as a superuser with psql -f; it starts the
-- VACUUM FULL itself with \! and reads the views back as the unprivileged
-- role, so it needs trust/peer auth and PGHOST/PGPORT in the environment.
-- The window is widened by an expression index over a deliberately slow
-- IMMUTABLE function, so the "rebuilding index" phase lasts about eight
-- seconds; nothing here depends on injection points.

DROP SCHEMA IF EXISTS j4_1 CASCADE;
DROP ROLE IF EXISTS j4_1_owner, j4_1_none;
CREATE ROLE j4_1_owner LOGIN;
CREATE ROLE j4_1_none LOGIN;
CREATE SCHEMA j4_1 AUTHORIZATION j4_1_owner;
GRANT USAGE ON SCHEMA j4_1 TO j4_1_none;

SET ROLE j4_1_owner;
-- IMMUTABLE is a lie, and only widens the window
CREATE FUNCTION j4_1.slow(x int) RETURNS int LANGUAGE plpgsql IMMUTABLE AS
$$ BEGIN PERFORM pg_sleep(0.002); RETURN x; END $$;
CREATE TABLE j4_1.t (id int PRIMARY KEY, payload text);
INSERT INTO j4_1.t SELECT g, 'p'||g FROM generate_series(1,4000) g;
CREATE INDEX t_slow ON j4_1.t (j4_1.slow(id));
RESET ROLE;

-- the onlooker holds nothing on the table and is in neither predefined role
SELECT has_table_privilege('j4_1_none','j4_1.t','SELECT')   AS none_select,
       pg_has_role('j4_1_none','pg_read_all_stats','USAGE') AS none_read_all_stats,
       pg_has_role('j4_1_none','j4_1_owner','USAGE')        AS none_is_owner;

-- start the rewrite as the owner and let it reach "rebuilding index"
\! psql -X -q -U j4_1_owner -d postgres -c "VACUUM FULL j4_1.t" &
SELECT pg_sleep(3);

-- what the owner (privileged for this row) sees
SELECT pid, relid::regclass, command, phase FROM pg_stat_progress_repack;
SELECT pid, relid::regclass, command, phase FROM pg_stat_progress_cluster;

-- what the onlooker sees:  repack correctly says nothing, cluster says CLUSTER
\! psql -X -U j4_1_none -d postgres -c "SELECT pid, relid, command, phase FROM pg_stat_progress_repack"
\! psql -X -U j4_1_none -d postgres -c "SELECT pid, relid, command, phase FROM pg_stat_progress_cluster"

SELECT pg_sleep(8);

-- the same thing without any server state: this is the redacted row's shape
SELECT CASE WHEN command IN ('CLUSTER', 'VACUUM FULL') THEN command
            WHEN repack_index_relid = 0 THEN 'VACUUM FULL'
            ELSE 'CLUSTER' END AS what_the_view_reports
  FROM (SELECT NULL::text AS command, NULL::oid AS repack_index_relid) redacted;

-- Cleanup
DROP SCHEMA j4_1 CASCADE;
DROP ROLE j4_1_owner, j4_1_none;
