From d9d3ecf561dc56cc92ad95b297d5ef88d15a8594 Mon Sep 17 00:00:00 2001 From: "Joshua D. Drake" Date: Fri, 16 Oct 2026 17:46:03 +0000 Subject: [PATCH 1/4] test: native_delete_visibility_paths' shell arms take the paths they name, as its twin does (#1339) The shell set enable_seqscan / enable_indexonlyscan and trusted the arm names, but none of the enable_* scan GUCs governs Custom Scan (PgColumnarScan). The index / bitmap and index-only arms planned as the columnar custom scan, the same path as the sequential arm. The pytest twin has forced each path with pgcolumnar.enable_custom_scan = off and asserted the node since it was written; its docstring reported the shell's gap. The shell now does the same, under the twin's premise names, and gains the twin's three other checks (the 5000 rows, more than one row group, and the rolled-back delete left the row). Red first: with pgcolumnar_fetch_row ignoring the delete mask, the old shell's "index / bitmap scan sees the live set" and "index-only scan sees the live set" stayed green. Both are now red in both twins, and the twins redden the same set by name under each mutation. The twin's "premise: the table holds all {ROWS} rows" becomes a literal name, which the grader needs. Co-Authored-By: Claude Opus 5.5 --- test/native_delete_visibility_paths.sh | 59 +++++++++++++++---- .../test_native_delete_visibility_paths.py | 5 +- 2 files changed, 50 insertions(+), 14 deletions(-) diff --git a/test/native_delete_visibility_paths.sh b/test/native_delete_visibility_paths.sh index 6cddc1c45..38776eb6d 100755 --- a/test/native_delete_visibility_paths.sh +++ b/test/native_delete_visibility_paths.sh @@ -26,8 +26,14 @@ q "CREATE EXTENSION IF NOT EXISTS pgcolumnar;" >/dev/null q "SET pgcolumnar.stripe_row_limit=1000; CREATE TABLE t (id int, v int) USING pgcolumnar; INSERT INTO t SELECT g, g FROM generate_series(1,5000) g; - CREATE UNIQUE INDEX t_id ON t (id); - DELETE FROM t WHERE id % 7 = 0;" >/dev/null + CREATE UNIQUE INDEX t_id ON t (id);" >/dev/null +check "premise: the table holds all 5000 rows" "$(q 'SELECT count(*) FROM t;')" "5000" +q "DELETE FROM t WHERE id % 7 = 0;" >/dev/null +# Each group builds its own mask, so one group would test the fold of one bitmap. +check "premise: the deletes span more than one row group" \ + "$(q "SELECT (count(*) >= 2)::int FROM pgcolumnar.storage s + JOIN pgcolumnar.row_group rg USING (storage_id) + WHERE s.relation_oid = 't'::regclass;")" "1" # Live count = 5000 - floor(5000/7) = 5000 - 714 = 4286. Five row groups. LIVE=4286 @@ -36,27 +42,58 @@ LIVE=4286 # multi-statement send. last() { q "$1" | tail -1; } -# A qual on the non-indexed column forces a real row-emitting sequential scan -# (which applies the delete mask per row), not the unqualified count(*) metadata -# fast-path (which subtracts delete counts and never reaches the bit test). +# EACH ARM FORCES THE PATH IT IS NAMED FOR, AND ASSERTS THE NODE FIRST (#1339). +# The arms set enable_seqscan/enable_indexonlyscan and trusted the names, but none +# of the enable_* scan GUCs governs Custom Scan (PgColumnarScan): measured, the +# index / bitmap and index-only arms planned as the columnar custom scan, the same +# path as the sequential arm, so three of four named paths were one path counted +# three times. pgcolumnar.enable_custom_scan=off is what moves the plan; the +# pytest twin has forced each path this way since it was written. +NO_COLUMNAR="SET pgcolumnar.enable_custom_scan = off;" +path_plan() { # path_plan SETS QUERY NODE -> yes, or the plan's node lines + local plan + plan="$(q "$1 EXPLAIN (COSTS OFF) $2")" + case "$plan" in + *"$3"*) echo yes ;; + *) echo "no: $(grep -E 'Scan|Aggregate' <<<"$plan" | sed 's/^ *//' | tr '\n' '/')" ;; + esac +} + +SEQ="$NO_COLUMNAR SET enable_indexscan=off; SET enable_bitmapscan=off; SET enable_indexonlyscan=off;" +check "premise: the sequential arm really plans a Seq Scan" \ + "$(path_plan "$SEQ" "SELECT count(*) FROM t WHERE v >= 0" "Seq Scan")" yes check "sequential scan sees the live set (row-emitting path)" \ - "$(last 'SET enable_indexscan=off; SET enable_bitmapscan=off; SET enable_indexonlyscan=off; SELECT count(*) FROM t WHERE v >= 0;')" "$LIVE" + "$(last "$SEQ SELECT count(*) FROM t WHERE v >= 0;")" "$LIVE" + +IDX="$NO_COLUMNAR SET enable_seqscan=off; SET enable_indexonlyscan=off;" +check "premise: the index arm really plans an Index Scan" \ + "$(path_plan "$IDX" "SELECT count(*) FROM t WHERE id >= 0" "Index Scan")" yes check "index / bitmap scan sees the live set" \ - "$(last 'SET enable_seqscan=off; SET enable_indexonlyscan=off; SELECT count(*) FROM t WHERE id >= 0;')" "$LIVE" + "$(last "$IDX SELECT count(*) FROM t WHERE id >= 0;")" "$LIVE" + +IOS="$NO_COLUMNAR SET enable_seqscan=off; SET enable_indexonlyscan=on;" +check "premise: the index-only arm really plans an Index Only Scan" \ + "$(path_plan "$IOS" "SELECT count(id) FROM t WHERE id >= 0" "Index Only Scan")" yes check "index-only scan sees the live set" \ - "$(last 'SET enable_seqscan=off; SET enable_indexonlyscan=on; SELECT count(id) FROM t WHERE id >= 0;')" "$LIVE" + "$(last "$IOS SELECT count(id) FROM t WHERE id >= 0;")" "$LIVE" + +# The columnar path, left to the planner: a different consumer of the same fold. +check "premise: the whole-table aggregate stays on the columnar path" \ + "$(path_plan "" "SELECT count(*) FROM t" "Custom Scan")" yes check "aggregate over the whole table sees the live set" \ "$(q 'SELECT count(*) FROM t;')" "$LIVE" # A specific deleted row is invisible, and its live neighbour visible, via the -# index fetch (which reaches the same bit test through a different path). +# index (which reaches the same bit test through a different path). check "a deleted row is invisible via the index" \ - "$(last 'SET enable_seqscan=off; SELECT count(*) FROM t WHERE id = 700;')" "0" + "$(last "$NO_COLUMNAR SET enable_seqscan=off; SELECT count(*) FROM t WHERE id = 700;")" "0" check "a live row is visible via the index" \ - "$(last 'SET enable_seqscan=off; SELECT count(*) FROM t WHERE id = 701;')" "1" + "$(last "$NO_COLUMNAR SET enable_seqscan=off; SELECT count(*) FROM t WHERE id = 701;")" "1" # The buffered (same-transaction) path: delete more inside a txn, then read it back. check "a row deleted in-transaction is invisible in the same transaction" \ "$(q 'BEGIN; DELETE FROM t WHERE id = 8; SELECT count(*) FROM t WHERE id = 8; ROLLBACK;' | grep -E '^[0-9]+$' | head -1)" "0" +# and the rollback really rolled back, so the arm above is not a delete that persisted +check "the rolled-back delete left the row in place" "$(q 'SELECT count(*) FROM t WHERE id = 8;')" "1" # A DELETE whose qual deletes another row of the same table (#1394). The inner DELETE's # scan start writes the outer statement's buffered marks, and its statement end writes diff --git a/test/pytest/test_native_delete_visibility_paths.py b/test/pytest/test_native_delete_visibility_paths.py index 28d39f414..3e443f4ae 100644 --- a/test/pytest/test_native_delete_visibility_paths.py +++ b/test/pytest/test_native_delete_visibility_paths.py @@ -33,8 +33,7 @@ index-only Index Only Scan 4286 So this port forces the path each arm is named for and ASSERTS THE NODE before reading -the count. The shell suite is unchanged -- reported rather than edited, since this pull -request adds no bash check. +the count. The shell suite now does the same, with the same premise names (#1339). """ import psycopg @@ -85,7 +84,7 @@ def test_native_delete_visibility_paths(pgc_conn, expect): cur.execute(f"INSERT INTO t SELECT g, g FROM generate_series(1,{ROWS}) g") cur.execute("CREATE UNIQUE INDEX t_id ON t (id)") cur.execute("SELECT count(*) FROM t") - expect.num(cur.fetchone()[0], ROWS, f"premise: the table holds all {ROWS} rows") + expect.num(cur.fetchone()[0], ROWS, "premise: the table holds all 5000 rows") cur.execute("DELETE FROM t WHERE id % 7 = 0") cur.execute( "SELECT count(*) FROM pgcolumnar.storage s " From 29c77e5524766fa35ec33859a677f79284216fd2 Mon Sep 17 00:00:00 2001 From: "Joshua D. Drake" Date: Fri, 16 Oct 2026 17:46:26 +0000 Subject: [PATCH 2/4] test: native_delete_visibility_paths dates five path arms and adds the twin's seven checks to the ledger (#1339) Census 4330 rows / 3019 never / 1311 dated. Co-Authored-By: Claude Opus 5.5 --- test/check_ledger.tsv | 19 +++++++++++++------ test/check_ledger_budget.txt | 15 ++++++++++++++- 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/test/check_ledger.tsv b/test/check_ledger.tsv index 37222b28a..6ffcdc5a5 100644 --- a/test/check_ledger.tsv +++ b/test/check_ledger.tsv @@ -2426,14 +2426,21 @@ native_decode_gating native_decode_gating the needle removes exactly the non-mat native_decode_gating native_decode_gating the needle skips exactly the vectors holding no match 15;16;17;18;19 2026-10-16 columnar_reader.c: the per-vector qual evaluation between the decode passes never runs;columnar_reader.c: the per-vector qual evaluator marks every vector skipped native_decode_gating native_decode_gating the nomatch query returns nothing 15;16;17;18;19 never - native_decode_gating native_decode_gating the zero-match query removes every row by filter, counted once 15;16;17;18;19 never - -native_delete_visibility_paths native_delete_visibility_paths a DELETE whose qual deletes another row of the table succeeds and leaves heap's rows (#1394) 15;16;17;18;19 2026-10-14 PgColumnarUpsertDeleteVector without the command advance (main before #1394's fix) -native_delete_visibility_paths native_delete_visibility_paths a deleted row is invisible via the index 15;16;17;18;19 never - +native_delete_visibility_paths native_delete_visibility_paths a DELETE whose qual deletes another row of the table succeeds and leaves heap's rows (#1394) 15;16;17;18;19 2026-10-16 PgColumnarUpsertDeleteVector without the command advance (main before #1394's fix);columnar_reader.c: the row-at-a-time scan reads a deleted row as live +native_delete_visibility_paths native_delete_visibility_paths a deleted row is invisible via the index 15;16;17;18;19 2026-10-16 columnar_reader.c: pgcolumnar_fetch_row ignores the flushed delete vector native_delete_visibility_paths native_delete_visibility_paths a live row is visible via the index 15;16;17;18;19 never - -native_delete_visibility_paths native_delete_visibility_paths a row deleted in-transaction is invisible in the same transaction 15;16;17;18;19 never - +native_delete_visibility_paths native_delete_visibility_paths a row deleted in-transaction is invisible in the same transaction 15;16;17;18;19 2026-10-16 columnar_reader.c: pgcolumnar_fetch_row ignores the flushed delete vector native_delete_visibility_paths native_delete_visibility_paths aggregate over the whole table sees the live set 15;16;17;18;19 never - -native_delete_visibility_paths native_delete_visibility_paths index / bitmap scan sees the live set 15;16;17;18;19 never - -native_delete_visibility_paths native_delete_visibility_paths index-only scan sees the live set 15;16;17;18;19 never - -native_delete_visibility_paths native_delete_visibility_paths sequential scan sees the live set (row-emitting path) 15;16;17;18;19 never - +native_delete_visibility_paths native_delete_visibility_paths index / bitmap scan sees the live set 15;16;17;18;19 2026-10-16 columnar_reader.c: pgcolumnar_fetch_row ignores the flushed delete vector +native_delete_visibility_paths native_delete_visibility_paths index-only scan sees the live set 15;16;17;18;19 2026-10-16 columnar_reader.c: pgcolumnar_fetch_row ignores the flushed delete vector +native_delete_visibility_paths native_delete_visibility_paths premise: the deletes span more than one row group 15;16;17;18;19 never - +native_delete_visibility_paths native_delete_visibility_paths premise: the index arm really plans an Index Scan 15;16;17;18;19 never - +native_delete_visibility_paths native_delete_visibility_paths premise: the index-only arm really plans an Index Only Scan 15;16;17;18;19 never - +native_delete_visibility_paths native_delete_visibility_paths premise: the sequential arm really plans a Seq Scan 15;16;17;18;19 never - +native_delete_visibility_paths native_delete_visibility_paths premise: the table holds all 5000 rows 15;16;17;18;19 never - +native_delete_visibility_paths native_delete_visibility_paths premise: the whole-table aggregate stays on the columnar path 15;16;17;18;19 never - +native_delete_visibility_paths native_delete_visibility_paths sequential scan sees the live set (row-emitting path) 15;16;17;18;19 2026-10-16 columnar_reader.c: the row-at-a-time scan reads a deleted row as live +native_delete_visibility_paths native_delete_visibility_paths the rolled-back delete left the row in place 15;16;17;18;19 never - native_dict_underfill native_dict_underfill backend survives the under-filled DICT chunk 15;16;17;18;19 2026-10-16 columnar_encoding.c: the DICT fill refusal replaced by a backend crash native_dict_underfill native_dict_underfill premise: the inflation rewrote the DICT chunk's descriptor 15;16;17;18;19 never - native_dict_underfill native_dict_underfill the text column is DICT-encoded (precondition) 15;16;17;18;19 never - diff --git a/test/check_ledger_budget.txt b/test/check_ledger_budget.txt index b2388c156..154cd50aa 100644 --- a/test/check_ledger_budget.txt +++ b/test/check_ledger_budget.txt @@ -3443,4 +3443,17 @@ suites_not_covered 193 # never 3010 + 7 = 3017 # dated 1300 + 6 = 1306 # 3017 + 1306 == 4323 -checks_never_observed_red 3017 +# +# #1339, native_delete_visibility_paths' shell arms take the paths they name +# (OffgridwithJD): 3017 -> 3019. Seven checks are new, all never: the six path and +# fixture premises the twin already had, and the rolled-back row. Five arms that +# entered never are now dated: the index / bitmap, index-only, deleted-row-via-index +# and in-transaction arms by pgcolumnar_fetch_row ignoring the delete vector, and +# the sequential arm by the row scan reading a deleted row as live. The #1394 arm +# gains the second in its history. +# +# ledger rows 4323 + 7 = 4330 +# never 3017 + 7 - 5 = 3019 +# dated 1306 + 5 = 1311 +# 3019 + 1311 == 4330 +checks_never_observed_red 3019 From e52bcf45e8322dcff7a9c472ec35cb9fe4f08fb6 Mon Sep 17 00:00:00 2001 From: "Joshua D. Drake" Date: Fri, 16 Oct 2026 17:56:36 +0000 Subject: [PATCH 3/4] test: maintenance_rollback's and maintenance_crash's "by index" readings read the index (#1339) Both suites' by_index forced the index with enable_seqscan off, which leaves the columnar custom scan on. Their whole-table range planned as Custom Scan (PgColumnarScan), so every "by scan and by index" arm compared the scan with itself. by_index now also turns the custom scan off, and each suite gains a premise that the reading is an Index Scan through its index, in both twins. Red first, maintenance_rollback, with pgcolumnar_index_fetch_tuple finding no row (the source fingerprint checked to differ from head): the old suite passes 13/13, and the new one reddens all six "by index" arms. The twins redden the same six by name. maintenance_crash compares the state after a crash with the state before it, both through by_index, so an index broken before the crash as well reddens nothing, by design: the arms catch drift the crash causes. Before this change the by-index leg was a second scan and could not see index drift at all. Co-Authored-By: Claude Opus 5.5 --- test/maintenance_crash.sh | 16 +++++++++++--- test/maintenance_rollback.sh | 14 +++++++++--- test/pytest/test_maintenance_crash.py | 16 ++++++++++++-- test/pytest/test_maintenance_rollback.py | 27 +++++++++++++++++------- 4 files changed, 57 insertions(+), 16 deletions(-) diff --git a/test/maintenance_crash.sh b/test/maintenance_crash.sh index 72d613be0..2026e931a 100755 --- a/test/maintenance_crash.sh +++ b/test/maintenance_crash.sh @@ -57,10 +57,16 @@ rows() { } RG_SQL="SELECT count(*) || '/' || coalesce(sum(row_count), 0) FROM pgcolumnar.row_group WHERE storage_id = pgcolumnar.get_storage_id('mc')" +BY_INDEX_SETS="SET enable_seqscan = off; SET enable_bitmapscan = off; SET pgcolumnar.enable_custom_scan = off;" +BY_INDEX_SQL="SELECT count(*) || '/' || md5(string_agg(id || ':' || v || ':' || w, ',' ORDER BY id)) + FROM mc WHERE id BETWEEN 1 AND 10000" by_index() { - q "SET enable_seqscan = off; SET enable_bitmapscan = off; - SELECT count(*) || '/' || md5(string_agg(id || ':' || v || ':' || w, ',' ORDER BY id)) - FROM mc WHERE id BETWEEN 1 AND 10000" | grep -E '^[0-9]+/' | tail -1 + # pgcolumnar.enable_custom_scan = off as well, and a premise that this plan is an + # Index Scan: enable_seqscan alone leaves the columnar custom scan on, and this + # whole-table range planned as Custom Scan (PgColumnarScan). Measured (#1339): + # with the index fetch reading the neighbouring row, every "by index" arm stayed + # green, because none of them read the index. + q "$BY_INDEX_SETS $BY_INDEX_SQL" | grep -E '^[0-9]+/' | tail -1 } state() { # the table, as "||" echo "$(rows)|$(q "$RG_SQL" | grep -E '^[0-9]+/' | tail -1)|$(by_index)" @@ -75,6 +81,10 @@ fresh_table() { CHECKPOINT;" >/dev/null } +fresh_table +check "premise: the by-index reading is an Index Scan through mc_id" \ + "$(pgc_uses_row_fetch "$BY_INDEX_SETS" "$BY_INDEX_SQL")" yes + FUNCS=("pgcolumnar.compact_rewrite('mc', 0.0)" "pgcolumnar.recluster('mc', 'v')" "pgcolumnar.compact('mc')" "pgcolumnar.vacuum('mc')") # ---- uncommitted at the crash --------------------------------------------------------- diff --git a/test/maintenance_rollback.sh b/test/maintenance_rollback.sh index ccc030146..0fc129182 100755 --- a/test/maintenance_rollback.sh +++ b/test/maintenance_rollback.sh @@ -40,14 +40,22 @@ rows() { # the rows, by content } RG_SQL="SELECT count(*) || '/' || coalesce(sum(row_count), 0) FROM pgcolumnar.row_group WHERE storage_id = pgcolumnar.get_storage_id('mr')" +BY_INDEX_SETS="SET enable_seqscan = off; SET enable_bitmapscan = off; SET pgcolumnar.enable_custom_scan = off;" +BY_INDEX_SQL="SELECT count(*) || '/' || md5(string_agg(id || ':' || v || ':' || w, ',' ORDER BY id)) + FROM mr WHERE id BETWEEN 1 AND 10000" by_index() { # the rows an index scan returns, by content - q "SET enable_seqscan = off; SET enable_bitmapscan = off; - SELECT count(*) || '/' || md5(string_agg(id || ':' || v || ':' || w, ',' ORDER BY id)) - FROM mr WHERE id BETWEEN 1 AND 10000" | grep -E '^[0-9]+/' | tail -1 + # pgcolumnar.enable_custom_scan = off as well, and a premise that this plan is an + # Index Scan: enable_seqscan alone leaves the columnar custom scan on, and this + # whole-table range planned as Custom Scan (PgColumnarScan). Measured (#1339): + # with the index fetch reading the neighbouring row, every "by index" arm stayed + # green, because none of them read the index. + q "$BY_INDEX_SETS $BY_INDEX_SQL" | grep -E '^[0-9]+/' | tail -1 } ROWS0="$(rows)" RG0="$(q "$RG_SQL")" check "premise: the table holds the rows it was given" "${ROWS0%%/*}|$RG0" "5400|10/10000" +check "premise: the by-index reading is an Index Scan through mr_id" \ + "$(pgc_uses_row_fetch "$BY_INDEX_SETS" "$BY_INDEX_SQL")" yes # rolled_back FUNCTION -> "|||" rolled_back() { diff --git a/test/pytest/test_maintenance_crash.py b/test/pytest/test_maintenance_crash.py index 97cdb0426..8fb17ee68 100644 --- a/test/pytest/test_maintenance_crash.py +++ b/test/pytest/test_maintenance_crash.py @@ -34,9 +34,13 @@ def _one(dsn, sql, setup=()): return f"ERROR: {str(exc).splitlines()[0]}" +BY_INDEX = ("SET enable_seqscan = off", "SET enable_bitmapscan = off", "SET pgcolumnar.enable_custom_scan = off") + + def _state(dsn): - by_index = _one(dsn, ROWS + " WHERE id BETWEEN 1 AND 10000", - ("SET enable_seqscan = off", "SET enable_bitmapscan = off")) + # the custom scan off too, and asserted by a premise: with only seqscan off this + # whole-table range planned as Custom Scan (PgColumnarScan) and read no index + by_index = _one(dsn, ROWS + " WHERE id BETWEEN 1 AND 10000", BY_INDEX) return f"{_one(dsn, ROWS)}|{_one(dsn, GROUPS)}|{by_index}" @@ -74,6 +78,14 @@ def test_a_crash_part_way_through_maintenance_loses_nothing(pgc_cluster, expect) with psycopg.connect(dsn, autocommit=True) as c: c.execute("CREATE EXTENSION IF NOT EXISTS pgcolumnar") + _fresh_table(dsn) + with psycopg.connect(dsn, autocommit=True) as c: + for st in BY_INDEX: + c.execute(st) + plan = [r[0] for r in c.execute("EXPLAIN (COSTS OFF) " + ROWS + " WHERE id BETWEEN 1 AND 10000")] + expect.text("yes" if any("Index Scan using" in line for line in plan) else "no", "yes", + "premise: the by-index reading is an Index Scan through mc_id") + for f in ("compact_rewrite('mc', 0.0)", "recluster('mc', 'v')", "compact('mc')", "vacuum('mc')"): name = f.split("(")[0] _fresh_table(dsn) diff --git a/test/pytest/test_maintenance_rollback.py b/test/pytest/test_maintenance_rollback.py index 9d4925209..0e4b97234 100644 --- a/test/pytest/test_maintenance_rollback.py +++ b/test/pytest/test_maintenance_rollback.py @@ -32,15 +32,25 @@ def _one(conn, sql): return f"ERROR: {str(exc).splitlines()[0]}" -def _by_index(conn): - with conn.cursor() as cur: - cur.execute("SET enable_seqscan = off") - cur.execute("SET enable_bitmapscan = off") - got = _one(conn, ROWS + " WHERE id BETWEEN 1 AND 10000") +BY_INDEX_SETS = ("enable_seqscan", "enable_bitmapscan", "pgcolumnar.enable_custom_scan") + + +def _by_index(conn, explain=False): + # the custom scan off too, and asserted by a premise: with only seqscan off this + # whole-table range planned as Custom Scan (PgColumnarScan) and read no index with conn.cursor() as cur: - cur.execute("RESET enable_seqscan") - cur.execute("RESET enable_bitmapscan") - return got + for g in BY_INDEX_SETS: + cur.execute(f"SET {g} = off") + try: + if explain: + with conn.cursor() as cur: + cur.execute("EXPLAIN (COSTS OFF) " + ROWS + " WHERE id BETWEEN 1 AND 10000") + return "yes" if any("Index Scan using" in r[0] for r in cur.fetchall()) else "no" + return _one(conn, ROWS + " WHERE id BETWEEN 1 AND 10000") + finally: + with conn.cursor() as cur: + for g in BY_INDEX_SETS: + cur.execute(f"RESET {g}") def _build_mr(conn): @@ -63,6 +73,7 @@ def test_a_maintenance_function_that_rolls_back_leaves_the_table_as_it_was(pgc_c rows0, groups0 = _one(pgc_conn, ROWS), _one(pgc_conn, GROUPS) expect.text(f"{rows0.split('/')[0]}|{groups0}", "5400|10/10000", "premise: the table holds the rows it was given") + expect.text(_by_index(pgc_conn, explain=True), "yes", "premise: the by-index reading is an Index Scan through mr_id") for f in funcs: name = f.split("(")[0] From d33011ba913509529cfed5bba98e3c72cce31e0b Mon Sep 17 00:00:00 2001 From: "Joshua D. Drake" Date: Fri, 16 Oct 2026 17:56:56 +0000 Subject: [PATCH 4/4] test: the maintenance suites' by-index premises join the ledger, and four rollback arms are dated (#1339) Census 4332 rows / 3017 never / 1315 dated. Co-Authored-By: Claude Opus 5.5 --- test/check_ledger.tsv | 14 ++++++++------ test/check_ledger_budget.txt | 13 ++++++++++++- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/test/check_ledger.tsv b/test/check_ledger.tsv index 6ffcdc5a5..2a9ea5c9b 100644 --- a/test/check_ledger.tsv +++ b/test/check_ledger.tsv @@ -2261,6 +2261,7 @@ maintenance_crash maintenance_crash compact_rewrite changed the row groups befor maintenance_crash maintenance_crash compact_rewrite changed the row groups in its open transaction 15;16;17;18;19 2026-10-15 compact_rewrite rewrites nothing (rewritten = 0) maintenance_crash maintenance_crash compact_rewrite committed, and the cluster came back after a crash before any checkpoint 15;16;17;18;19 never - maintenance_crash maintenance_crash compact_rewrite ran in its open transaction, and the cluster came back after a crash 15;16;17;18;19 never - +maintenance_crash maintenance_crash premise: the by-index reading is an Index Scan through mc_id 15;16;17;18;19 never - maintenance_crash maintenance_crash recluster changed the row groups before the crash 15;16;17;18;19 never - maintenance_crash maintenance_crash recluster changed the row groups in its open transaction 15;16;17;18;19 never - maintenance_crash maintenance_crash recluster committed, and the cluster came back after a crash before any checkpoint 15;16;17;18;19 never - @@ -2269,15 +2270,16 @@ maintenance_crash maintenance_crash vacuum changed the row groups before the cra maintenance_crash maintenance_crash vacuum changed the row groups in its open transaction 15;16;17;18;19 never - maintenance_crash maintenance_crash vacuum committed, and the cluster came back after a crash before any checkpoint 15;16;17;18;19 never - maintenance_crash maintenance_crash vacuum ran in its open transaction, and the cluster came back after a crash 15;16;17;18;19 never - -maintenance_rollback maintenance_rollback after a savepoint rolls back two rewrites, the rows written later in the transaction commit beside the old ones 15;16;17;18;19 2026-10-15 PgColumnarAllocateFreeSpace reuses space whatever its freeing xid, so a rolled-back maintenance function's freed bytes are overwritten -maintenance_rollback maintenance_rollback after compact rolls back, the table holds the same rows and row groups, by scan and by index 15;16;17;18;19 never - -maintenance_rollback maintenance_rollback after compact_rewrite rolls back, the table holds the same rows and row groups, by scan and by index 15;16;17;18;19 2026-10-15 PgColumnarAllocateFreeSpace reuses space whatever its freeing xid, so a rolled-back maintenance function's freed bytes are overwritten -maintenance_rollback maintenance_rollback after recluster rolls back, the table holds the same rows and row groups, by scan and by index 15;16;17;18;19 never - -maintenance_rollback maintenance_rollback after vacuum rolls back, the table holds the same rows and row groups, by scan and by index 15;16;17;18;19 never - -maintenance_rollback maintenance_rollback after vacuum_sorted rolls back, the table holds the same rows and row groups, by scan and by index 15;16;17;18;19 never - +maintenance_rollback maintenance_rollback after a savepoint rolls back two rewrites, the rows written later in the transaction commit beside the old ones 15;16;17;18;19 2026-10-16 PgColumnarAllocateFreeSpace reuses space whatever its freeing xid, so a rolled-back maintenance function's freed bytes are overwritten;columnar_tableam.c: pgcolumnar_index_fetch_tuple finds no row +maintenance_rollback maintenance_rollback after compact rolls back, the table holds the same rows and row groups, by scan and by index 15;16;17;18;19 2026-10-16 columnar_tableam.c: pgcolumnar_index_fetch_tuple finds no row +maintenance_rollback maintenance_rollback after compact_rewrite rolls back, the table holds the same rows and row groups, by scan and by index 15;16;17;18;19 2026-10-16 PgColumnarAllocateFreeSpace reuses space whatever its freeing xid, so a rolled-back maintenance function's freed bytes are overwritten;columnar_tableam.c: pgcolumnar_index_fetch_tuple finds no row +maintenance_rollback maintenance_rollback after recluster rolls back, the table holds the same rows and row groups, by scan and by index 15;16;17;18;19 2026-10-16 columnar_tableam.c: pgcolumnar_index_fetch_tuple finds no row +maintenance_rollback maintenance_rollback after vacuum rolls back, the table holds the same rows and row groups, by scan and by index 15;16;17;18;19 2026-10-16 columnar_tableam.c: pgcolumnar_index_fetch_tuple finds no row +maintenance_rollback maintenance_rollback after vacuum_sorted rolls back, the table holds the same rows and row groups, by scan and by index 15;16;17;18;19 2026-10-16 columnar_tableam.c: pgcolumnar_index_fetch_tuple finds no row maintenance_rollback maintenance_rollback and every function then runs for real and keeps the same rows 15;16;17;18;19 never - maintenance_rollback maintenance_rollback compact changed the row groups inside its transaction 15;16;17;18;19 never - maintenance_rollback maintenance_rollback compact_rewrite changed the row groups inside its transaction 15;16;17;18;19 never - +maintenance_rollback maintenance_rollback premise: the by-index reading is an Index Scan through mr_id 15;16;17;18;19 never - maintenance_rollback maintenance_rollback premise: the table holds the rows it was given 15;16;17;18;19 never - maintenance_rollback maintenance_rollback recluster changed the row groups inside its transaction 15;16;17;18;19 never - maintenance_rollback maintenance_rollback vacuum changed the row groups inside its transaction 15;16;17;18;19 never - diff --git a/test/check_ledger_budget.txt b/test/check_ledger_budget.txt index 154cd50aa..affa347e8 100644 --- a/test/check_ledger_budget.txt +++ b/test/check_ledger_budget.txt @@ -3456,4 +3456,15 @@ suites_not_covered 193 # never 3017 + 7 - 5 = 3019 # dated 1306 + 5 = 1311 # 3019 + 1311 == 4330 -checks_never_observed_red 3019 +# +# #1339, maintenance_rollback's and maintenance_crash's by-index readings read the +# index (OffgridwithJD): 3019 -> 3017. Two premises are new, one per suite, both +# never. With pgcolumnar_index_fetch_tuple finding no row, maintenance_rollback's +# six "by index" arms are red; four of them entered never and are now dated, and +# the other two gain the mutation in their history. No row is removed. +# +# ledger rows 4330 + 2 = 4332 +# never 3019 + 2 - 4 = 3017 +# dated 1311 + 4 = 1315 +# 3017 + 1315 == 4332 +checks_never_observed_red 3017