From f590ba51fc51ee9429ffe8d27b7dc3f705d5cc69 Mon Sep 17 00:00:00 2001 From: "Joshua D. Drake" Date: Wed, 14 Oct 2026 00:23:55 +0000 Subject: [PATCH 1/3] test: expire refuses a non-owner, a non-time retention column and a dropped one, with rows it could drop, in both twins (#1341) Co-Authored-By: Claude Opus 5.5 --- test/pytest/TESTS.md | 1 + test/pytest/expected_tests.txt | 4 +- test/pytest/test_ttl_expire.py | 71 ++++++++++++++++++++++++++++++++++ test/ttl_expire.sh | 45 +++++++++++++++++++++ 4 files changed, 120 insertions(+), 1 deletion(-) diff --git a/test/pytest/TESTS.md b/test/pytest/TESTS.md index d8b9ce5cf..e0004dd29 100644 --- a/test/pytest/TESTS.md +++ b/test/pytest/TESTS.md @@ -6228,6 +6228,7 @@ restored byte-identical. | test | what it pins | | --- | --- | | `test_expire_retires_expired_groups_and_keeps_the_straddling_one` | the feature, and the straddling group that constrains it | +| `test_expire_refuses_what_it_cannot_honour` | #1341: a non-owner holding DELETE (`42501`), a retention column that is not a time type (`42804`) and a dropped one (`42703`) are refused with every row still there, on tables whose rows are all past retention; the owner's `expire` is the control that retires them | | `test_a_null_retention_value_keeps_its_group` | a live NULL the maximum cannot speak for | | `test_an_index_only_scan_does_not_return_retired_rows` | the visibility-map bits over a group `expire` removed | | `test_a_non_positive_retention_is_refused` | a cutoff in the future would retire live groups: `22023`, with a positive control | diff --git a/test/pytest/expected_tests.txt b/test/pytest/expected_tests.txt index 0d3048d8d..32ad63ece 100644 --- a/test/pytest/expected_tests.txt +++ b/test/pytest/expected_tests.txt @@ -629,4 +629,6 @@ guard_tests 429 # kind and the --partition mode), by collection over the cluster files on this branch. # cluster_tests 532 -> 533 for #1339's test_a_timestamp_retention_column_uses_the_sessions_local_time # in test_ttl_expire.py, by collection over the cluster files on this branch. -cluster_tests 533 +# cluster_tests 533 -> 534 for #1341's test_expire_refuses_what_it_cannot_honour in +# test_ttl_expire.py, by collection over the cluster files on this tree. +cluster_tests 534 diff --git a/test/pytest/test_ttl_expire.py b/test/pytest/test_ttl_expire.py index 5e3c2d5a1..09bfa1fd5 100644 --- a/test/pytest/test_ttl_expire.py +++ b/test/pytest/test_ttl_expire.py @@ -388,6 +388,77 @@ def test_a_non_positive_retention_is_refused(pgc_conn, expect): "control: and the rows are all still there") +def test_expire_refuses_what_it_cannot_honour(pgc_conn, expect): + """A non-owner, a retention column that is not a time type, and a dropped one. + + Each refused table holds 3,000 rows ten days old, every one past a one-day + retention, so a missing refusal is not a quiet no-op: it is retired groups. The + row count beside each SQLSTATE is what says nothing was dropped (#1341). + """ + with pgc_conn.cursor() as cur: + cur.execute("DO $$ BEGIN CREATE ROLE ttl_nobody; " + "EXCEPTION WHEN duplicate_object THEN NULL; END $$") + cur.execute("GRANT USAGE ON SCHEMA pgcolumnar TO ttl_nobody") + cur.execute("SELECT current_schema()") + cur.execute(f'GRANT USAGE ON SCHEMA "{cur.fetchone()[0]}" TO ttl_nobody') + for t, ty in (("ttl_own", "timestamptz"), ("ttl_int", "int"), ("ttl_gone", "timestamptz")): + cur.execute(f"CREATE TABLE {t} (id int, ts {ty}, ts2 timestamptz) USING pgcolumnar") + cur.execute(f"SELECT pgcolumnar.set_options('{t}', stripe_row_limit => 1000)") + cur.execute("INSERT INTO ttl_own SELECT g, now() - interval '10 days', now()" + " FROM generate_series(1,3000) g") + cur.execute("INSERT INTO ttl_int SELECT g, g - 1500, now() FROM generate_series(1,3000) g") + cur.execute("INSERT INTO ttl_gone SELECT g, now() - interval '10 days', now() - interval '10 days'" + " FROM generate_series(1,3000) g") + def count(t): + return _one(pgc_conn, f"SELECT count(*) FROM {t}") + + def groups(t): + return _one(pgc_conn, "SELECT count(*) FROM pgcolumnar.row_group" + " WHERE storage_id = pgcolumnar.get_storage_id(%s::regclass)", (t,)) + + lay = "".join(f"{groups(t)}:{count(t)} " for t in ("ttl_gone", "ttl_int", "ttl_own")) + expect.text(lay, "3:3000 3:3000 3:3000 ", + "premise: each refused table holds 3000 rows in three row groups") + + # Ownership: DELETE on the table is not enough, expire drops whole row groups. + with pgc_conn.cursor() as cur: + cur.execute("SELECT pgcolumnar.set_options('ttl_own', ttl_column => 'ts', ttl_interval => '1 day')") + cur.execute("GRANT SELECT, DELETE ON ttl_own TO ttl_nobody") + cur.execute("SET ROLE ttl_nobody") + try: + state = _sqlstate(pgc_conn, "SELECT pgcolumnar.expire('ttl_own')") + finally: + with pgc_conn.cursor() as cur: + cur.execute("RESET ROLE") + expect.text(f"{state}|{count('ttl_own')}", "42501|3000", + "a role that does not own the table cannot expire it, even holding DELETE (42501)") + retired = _one(pgc_conn, "SELECT pgcolumnar.expire('ttl_own')") + expect.text(f"{retired}|{count('ttl_own')}", "3|0", + "control: and its owner's expire retires every aged group") + + # Type: set_options takes a column of any type, and expire is what refuses one + # that is not date, timestamp or timestamptz. + expect.text( + _sqlstate(pgc_conn, "SELECT pgcolumnar.set_options('ttl_int', ttl_column => 'ts'," + " ttl_interval => '1 day')") or "accepted", + "accepted", "set_options accepts an int retention column, so the refusal is expire's") + int_state = _sqlstate(pgc_conn, "SELECT pgcolumnar.expire('ttl_int')") + expect.text(f"{int_state}|{count('ttl_int')}", "42804|3000", + "expire refuses a retention column that is not a time type (42804)") + + # A dropped retention column: the declaration outlives it, and expire refuses + # rather than reading some other column. + with pgc_conn.cursor() as cur: + cur.execute("SELECT pgcolumnar.set_options('ttl_gone', ttl_column => 'ts', ttl_interval => '1 day')") + cur.execute("ALTER TABLE ttl_gone DROP COLUMN ts") + expect.text(_one(pgc_conn, "SELECT ttl_column FROM pgcolumnar.options" + " WHERE regclass = 'ttl_gone'::regclass"), "ts", + "premise: the declaration still names the dropped column") + gone_state = _sqlstate(pgc_conn, "SELECT pgcolumnar.expire('ttl_gone')") + expect.text(f"{gone_state}|{count('ttl_gone')}", "42703|3000", + "expire refuses a retention column that no longer exists (42703)") + + def test_a_deleted_null_stops_pinning_its_group(pgc_conn, expect): """`null_count` is recorded at WRITE time and never revised. diff --git a/test/ttl_expire.sh b/test/ttl_expire.sh index 3ebd1c875..2477df786 100755 --- a/test/ttl_expire.sh +++ b/test/ttl_expire.sh @@ -262,6 +262,51 @@ check "control: and a positive one is still accepted" \ check "control: and the rows are all still there" \ "$(q 'SELECT count(*) FROM ttl_neg')" "100" +# ---- what expire refuses, with rows it could have dropped (#1341) ---------- +# +# Each refused table holds 3,000 rows ten days old, every one past a one-day +# retention, so a missing refusal is not a quiet no-op: it is retired groups. The +# row count beside each SQLSTATE is what says nothing was dropped. +psql_run "CREATE ROLE ttl_nobody; + GRANT USAGE ON SCHEMA pgcolumnar TO ttl_nobody;" +for _t in ttl_own ttl_int ttl_gone; do + _ty=timestamptz; [ "$_t" = ttl_int ] && _ty=int + psql_run "CREATE TABLE $_t (id int, ts $_ty, ts2 timestamptz) USING pgcolumnar; + SELECT pgcolumnar.set_options('$_t', stripe_row_limit => 1000);" +done +psql_run "INSERT INTO ttl_own SELECT g, now() - interval '10 days', now() FROM generate_series(1,3000) g; + INSERT INTO ttl_int SELECT g, g - 1500, now() FROM generate_series(1,3000) g; + INSERT INTO ttl_gone SELECT g, now() - interval '10 days', now() - interval '10 days' FROM generate_series(1,3000) g;" +_lay="" +for _t in ttl_gone ttl_int ttl_own; do + _lay="$_lay$(q "SELECT count(*) FROM pgcolumnar.row_group WHERE storage_id = pgcolumnar.get_storage_id('$_t');"):$(q "SELECT count(*) FROM $_t;") " +done +check "premise: each refused table holds 3000 rows in three row groups" "$_lay" "3:3000 3:3000 3:3000 " + +# Ownership: DELETE on the table is not enough, expire drops whole row groups. +psql_run "SELECT pgcolumnar.set_options('ttl_own', ttl_column => 'ts', ttl_interval => '1 day'); + GRANT SELECT, DELETE ON ttl_own TO ttl_nobody;" +check "a role that does not own the table cannot expire it, even holding DELETE (42501)" \ + "$(ttl_state "SET ROLE ttl_nobody; SELECT pgcolumnar.expire('ttl_own');")|$(q 'SELECT count(*) FROM ttl_own')" "42501|3000" +check "control: and its owner's expire retires every aged group" \ + "$(q "SELECT pgcolumnar.expire('ttl_own');")|$(q 'SELECT count(*) FROM ttl_own')" "3|0" + +# Type: set_options takes a column of any type, and expire is what refuses one +# that is not date, timestamp or timestamptz. +check "set_options accepts an int retention column, so the refusal is expire's" \ + "$(ttl_state "SELECT pgcolumnar.set_options('ttl_int', ttl_column => 'ts', ttl_interval => '1 day');")" "" +check "expire refuses a retention column that is not a time type (42804)" \ + "$(ttl_state "SELECT pgcolumnar.expire('ttl_int');")|$(q 'SELECT count(*) FROM ttl_int')" "42804|3000" + +# A dropped retention column: the declaration outlives it, and expire refuses +# rather than reading some other column. +psql_run "SELECT pgcolumnar.set_options('ttl_gone', ttl_column => 'ts', ttl_interval => '1 day'); + ALTER TABLE ttl_gone DROP COLUMN ts;" +check "premise: the declaration still names the dropped column" \ + "$(q "SELECT ttl_column FROM pgcolumnar.options WHERE regclass = 'ttl_gone'::regclass;")" "ts" +check "expire refuses a retention column that no longer exists (42703)" \ + "$(ttl_state "SELECT pgcolumnar.expire('ttl_gone');")|$(q 'SELECT count(*) FROM ttl_gone')" "42703|3000" + # ---- a NULL that has been DELETED must stop pinning its group --------------- # # z->nullCount is recorded at WRITE time and never revised, so it still counts From fed6aef927f7fbd7bb3951c87fda5ecb1687e798 Mon Sep 17 00:00:00 2001 From: "Joshua D. Drake" Date: Wed, 14 Oct 2026 00:35:10 +0000 Subject: [PATCH 2/3] test: the owner's expire control gets its own table, out of reach of the refusal it controls (#1341) Co-Authored-By: Claude Opus 5.5 --- test/pytest/test_ttl_expire.py | 25 +++++++++++++++---------- test/ttl_expire.sh | 14 +++++++++----- 2 files changed, 24 insertions(+), 15 deletions(-) diff --git a/test/pytest/test_ttl_expire.py b/test/pytest/test_ttl_expire.py index 09bfa1fd5..7c7d252af 100644 --- a/test/pytest/test_ttl_expire.py +++ b/test/pytest/test_ttl_expire.py @@ -393,7 +393,9 @@ def test_expire_refuses_what_it_cannot_honour(pgc_conn, expect): Each refused table holds 3,000 rows ten days old, every one past a one-day retention, so a missing refusal is not a quiet no-op: it is retired groups. The - row count beside each SQLSTATE is what says nothing was dropped (#1341). + row count beside each SQLSTATE is what says nothing was dropped (#1341). ttl_ctl + is the same fixture as ttl_own on its own table, so the owner's control cannot be + reached by whatever a missing refusal did to ttl_own. """ with pgc_conn.cursor() as cur: cur.execute("DO $$ BEGIN CREATE ROLE ttl_nobody; " @@ -401,11 +403,13 @@ def test_expire_refuses_what_it_cannot_honour(pgc_conn, expect): cur.execute("GRANT USAGE ON SCHEMA pgcolumnar TO ttl_nobody") cur.execute("SELECT current_schema()") cur.execute(f'GRANT USAGE ON SCHEMA "{cur.fetchone()[0]}" TO ttl_nobody') - for t, ty in (("ttl_own", "timestamptz"), ("ttl_int", "int"), ("ttl_gone", "timestamptz")): + for t, ty in (("ttl_own", "timestamptz"), ("ttl_ctl", "timestamptz"), ("ttl_int", "int"), + ("ttl_gone", "timestamptz")): cur.execute(f"CREATE TABLE {t} (id int, ts {ty}, ts2 timestamptz) USING pgcolumnar") cur.execute(f"SELECT pgcolumnar.set_options('{t}', stripe_row_limit => 1000)") - cur.execute("INSERT INTO ttl_own SELECT g, now() - interval '10 days', now()" - " FROM generate_series(1,3000) g") + for t in ("ttl_own", "ttl_ctl"): + cur.execute(f"INSERT INTO {t} SELECT g, now() - interval '10 days', now()" + " FROM generate_series(1,3000) g") cur.execute("INSERT INTO ttl_int SELECT g, g - 1500, now() FROM generate_series(1,3000) g") cur.execute("INSERT INTO ttl_gone SELECT g, now() - interval '10 days', now() - interval '10 days'" " FROM generate_series(1,3000) g") @@ -416,13 +420,14 @@ def groups(t): return _one(pgc_conn, "SELECT count(*) FROM pgcolumnar.row_group" " WHERE storage_id = pgcolumnar.get_storage_id(%s::regclass)", (t,)) - lay = "".join(f"{groups(t)}:{count(t)} " for t in ("ttl_gone", "ttl_int", "ttl_own")) - expect.text(lay, "3:3000 3:3000 3:3000 ", - "premise: each refused table holds 3000 rows in three row groups") + lay = "".join(f"{groups(t)}:{count(t)} " for t in ("ttl_ctl", "ttl_gone", "ttl_int", "ttl_own")) + expect.text(lay, "3:3000 3:3000 3:3000 3:3000 ", + "premise: each table holds 3000 rows in three row groups") # Ownership: DELETE on the table is not enough, expire drops whole row groups. with pgc_conn.cursor() as cur: - cur.execute("SELECT pgcolumnar.set_options('ttl_own', ttl_column => 'ts', ttl_interval => '1 day')") + for t in ("ttl_own", "ttl_ctl"): + cur.execute(f"SELECT pgcolumnar.set_options('{t}', ttl_column => 'ts', ttl_interval => '1 day')") cur.execute("GRANT SELECT, DELETE ON ttl_own TO ttl_nobody") cur.execute("SET ROLE ttl_nobody") try: @@ -432,8 +437,8 @@ def groups(t): cur.execute("RESET ROLE") expect.text(f"{state}|{count('ttl_own')}", "42501|3000", "a role that does not own the table cannot expire it, even holding DELETE (42501)") - retired = _one(pgc_conn, "SELECT pgcolumnar.expire('ttl_own')") - expect.text(f"{retired}|{count('ttl_own')}", "3|0", + retired = _one(pgc_conn, "SELECT pgcolumnar.expire('ttl_ctl')") + expect.text(f"{retired}|{count('ttl_ctl')}", "3|0", "control: and its owner's expire retires every aged group") # Type: set_options takes a column of any type, and expire is what refuses one diff --git a/test/ttl_expire.sh b/test/ttl_expire.sh index 2477df786..65a6db7e1 100755 --- a/test/ttl_expire.sh +++ b/test/ttl_expire.sh @@ -266,30 +266,34 @@ check "control: and the rows are all still there" \ # # Each refused table holds 3,000 rows ten days old, every one past a one-day # retention, so a missing refusal is not a quiet no-op: it is retired groups. The -# row count beside each SQLSTATE is what says nothing was dropped. +# row count beside each SQLSTATE is what says nothing was dropped. ttl_ctl is the +# same fixture as ttl_own on its own table, so the owner's control cannot be +# reached by whatever a missing refusal did to ttl_own. psql_run "CREATE ROLE ttl_nobody; GRANT USAGE ON SCHEMA pgcolumnar TO ttl_nobody;" -for _t in ttl_own ttl_int ttl_gone; do +for _t in ttl_own ttl_ctl ttl_int ttl_gone; do _ty=timestamptz; [ "$_t" = ttl_int ] && _ty=int psql_run "CREATE TABLE $_t (id int, ts $_ty, ts2 timestamptz) USING pgcolumnar; SELECT pgcolumnar.set_options('$_t', stripe_row_limit => 1000);" done psql_run "INSERT INTO ttl_own SELECT g, now() - interval '10 days', now() FROM generate_series(1,3000) g; + INSERT INTO ttl_ctl SELECT g, now() - interval '10 days', now() FROM generate_series(1,3000) g; INSERT INTO ttl_int SELECT g, g - 1500, now() FROM generate_series(1,3000) g; INSERT INTO ttl_gone SELECT g, now() - interval '10 days', now() - interval '10 days' FROM generate_series(1,3000) g;" _lay="" -for _t in ttl_gone ttl_int ttl_own; do +for _t in ttl_ctl ttl_gone ttl_int ttl_own; do _lay="$_lay$(q "SELECT count(*) FROM pgcolumnar.row_group WHERE storage_id = pgcolumnar.get_storage_id('$_t');"):$(q "SELECT count(*) FROM $_t;") " done -check "premise: each refused table holds 3000 rows in three row groups" "$_lay" "3:3000 3:3000 3:3000 " +check "premise: each table holds 3000 rows in three row groups" "$_lay" "3:3000 3:3000 3:3000 3:3000 " # Ownership: DELETE on the table is not enough, expire drops whole row groups. psql_run "SELECT pgcolumnar.set_options('ttl_own', ttl_column => 'ts', ttl_interval => '1 day'); + SELECT pgcolumnar.set_options('ttl_ctl', ttl_column => 'ts', ttl_interval => '1 day'); GRANT SELECT, DELETE ON ttl_own TO ttl_nobody;" check "a role that does not own the table cannot expire it, even holding DELETE (42501)" \ "$(ttl_state "SET ROLE ttl_nobody; SELECT pgcolumnar.expire('ttl_own');")|$(q 'SELECT count(*) FROM ttl_own')" "42501|3000" check "control: and its owner's expire retires every aged group" \ - "$(q "SELECT pgcolumnar.expire('ttl_own');")|$(q 'SELECT count(*) FROM ttl_own')" "3|0" + "$(q "SELECT pgcolumnar.expire('ttl_ctl');")|$(q 'SELECT count(*) FROM ttl_ctl')" "3|0" # Type: set_options takes a column of any type, and expire is what refuses one # that is not date, timestamp or timestamptz. From cc5557b56b8aba86393194676b122d1c5971a1d3 Mon Sep 17 00:00:00 2001 From: "Joshua D. Drake" Date: Wed, 14 Oct 2026 02:37:49 +0000 Subject: [PATCH 3/3] test: ledger rows for expire's refusal arms, from the files merged with #1353 and #1355 (#1341) Co-Authored-By: Claude Opus 5.5 --- test/check_ledger.tsv | 7 +++++++ test/check_ledger_budget.txt | 16 +++++++++++++++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/test/check_ledger.tsv b/test/check_ledger.tsv index 6255ed994..f03d96374 100644 --- a/test/check_ledger.tsv +++ b/test/check_ledger.tsv @@ -2265,6 +2265,7 @@ ttl_expire ttl_expire a group whose only NULLs have been deleted is retired 15;1 ttl_expire ttl_expire a later expire on a column of only NULLs reads no zone map 15;16;17;18;19 2026-10-13 expire's early exit removed (the tree before #1310);the early exit not taken for a column with no value ttl_expire ttl_expire a negative ttl_interval is refused with 22023 15;16;17;18;19 never - ttl_expire ttl_expire a no-op expire reads no zone map when the extent's minimum is past the cutoff 15;16;17;18;19 2026-10-13 expire's early exit removed (the tree before #1310) +ttl_expire ttl_expire a role that does not own the table cannot expire it, even holding DELETE (42501) 15;16;17;18;19 2026-10-14 expire without its owner check ttl_expire ttl_expire a rolled-back expire records nothing 15;16;17;18;19 never - ttl_expire ttl_expire a row dated exactly on the cutoff is kept, so truncation errs toward keeping 15;16;17;18;19 never - ttl_expire ttl_expire a table with no declared retention is an error, not a silent success 15;16;17;18;19 2026-10-14 the no-retention refusal raised as XX000 rather than 55000 @@ -2283,6 +2284,7 @@ ttl_expire ttl_expire but the zone map still records the deleted NULLs 15;16;17; ttl_expire ttl_expire compact_rewrite retired a group 15;16;17;18;19 2026-10-14 compact_rewrite returning 0 before rewriting anything ttl_expire ttl_expire control: a group whose NULLs are still live is NOT retired 15;16;17;18;19 never - ttl_expire ttl_expire control: and a positive one is still accepted 15;16;17;18;19 never - +ttl_expire ttl_expire control: and its owner's expire retires every aged group 15;16;17;18;19 never - ttl_expire ttl_expire control: and the rows are all still there 15;16;17;18;19 never - ttl_expire ttl_expire control: and those NULL rows survive 15;16;17;18;19 never - ttl_expire ttl_expire dropping the table drops its retention record 15;16;17;18;19 never - @@ -2295,6 +2297,8 @@ ttl_expire ttl_expire expire in the transaction that wrote aged rows retires the ttl_expire ttl_expire expire on a date column retires at least one group 15;16;17;18;19 2026-10-14 expire returning 0 whatever it retired;the early exit compares the extent's maximum to the cutoff instead of its minimum ttl_expire ttl_expire expire on a timestamp column retires the groups wholly before the session's local cutoff 15;16;17;18;19 2026-10-14 expire's timestamp cutoff taken from the UTC wall clock, not the session's local time ttl_expire ttl_expire expire records exactly the groups it retired 15;16;17;18;19 2026-10-13 the early exit compares the extent's maximum to the cutoff instead of its minimum +ttl_expire ttl_expire expire refuses a retention column that is not a time type (42804) 15;16;17;18;19 2026-10-14 expire returning 0 for a retention column that is not a time type +ttl_expire ttl_expire expire refuses a retention column that no longer exists (42703) 15;16;17;18;19 2026-10-14 expire returning 0 for a retention column that no longer exists ttl_expire ttl_expire expire retired the three aged groups 15;16;17;18;19 2026-10-14 expire returning 0 whatever it retired;the early exit compares the extent's maximum to the cutoff instead of its minimum ttl_expire ttl_expire expire retires at least one group (#403 item 5a) 15;16;17;18;19 2026-10-14 expire returning 0 whatever it retired;the early exit compares the extent's maximum to the cutoff instead of its minimum ttl_expire ttl_expire expire retires the aged groups and keeps those of NULLs 15;16;17;18;19 2026-10-14 expire returning 0 whatever it retired @@ -2304,6 +2308,7 @@ ttl_expire ttl_expire premise: an index-only scan is chosen on the all-visible t ttl_expire ttl_expire premise: and every live row is past the retention 15;16;17;18;19 never - ttl_expire ttl_expire premise: and rows dated exactly ON the cutoff, which decide the rounding 15;16;17;18;19 never - ttl_expire ttl_expire premise: and the SAME row group holds expired rows and NULL ones 15;16;17;18;19 never - +ttl_expire ttl_expire premise: each table holds 3000 rows in three row groups 15;16;17;18;19 never - ttl_expire ttl_expire premise: in UTC the fixture straddles that session's cutoff 15;16;17;18;19 never - ttl_expire ttl_expire premise: in a zone on another date the fixture straddles that session's cutoff 15;16;17;18;19 never - ttl_expire ttl_expire premise: no LIVE row holds a NULL retention value any more 15;16;17;18;19 never - @@ -2313,6 +2318,7 @@ ttl_expire ttl_expire premise: rows exist on both sides of the cutoff within one ttl_expire ttl_expire premise: some rows are past the retention and some are not 15;16;17;18;19 never - ttl_expire ttl_expire premise: the date fixture has rows on both sides of the cutoff 15;16;17;18;19 never - ttl_expire ttl_expire premise: the date fixture is laid out in several row groups 15;16;17;18;19 never - +ttl_expire ttl_expire premise: the declaration still names the dropped column 15;16;17;18;19 never - ttl_expire ttl_expire premise: the fixture is ONE row group, so the NULLs share it 15;16;17;18;19 never - ttl_expire ttl_expire premise: the fixture is laid out in several row groups 15;16;17;18;19 never - ttl_expire ttl_expire premise: the no-op fixture has many row groups 15;16;17;18;19 never - @@ -2322,6 +2328,7 @@ ttl_expire ttl_expire premise: the table holds NULL retention rows at all 15;16; ttl_expire ttl_expire premise: the timestamp fixture is six groups of 1,000 rows 15;16;17;18;19 never - ttl_expire ttl_expire running it again retires nothing, because nothing new expired 15;16;17;18;19 never - ttl_expire ttl_expire seqscan agrees the expired rows are gone 15;16;17;18;19 never - +ttl_expire ttl_expire set_options accepts an int retention column, so the refusal is expire's 15;16;17;18;19 never - ttl_expire ttl_expire the index-only scan is still the plan after expire 15;16;17;18;19 never - ttl_expire ttl_expire the retention is recorded where the other options live 15;16;17;18;19 never - ttl_expire ttl_expire with the extent, expire retires the same groups 15;16;17;18;19 2026-10-13 the early exit compares the extent's maximum to the cutoff instead of its minimum diff --git a/test/check_ledger_budget.txt b/test/check_ledger_budget.txt index ff626ea71..c15baae89 100644 --- a/test/check_ledger_budget.txt +++ b/test/check_ledger_budget.txt @@ -1871,4 +1871,18 @@ suites_not_covered 246 # never 1798 - 1 = 1797 # dated 555 + 2 = 557 # 1797 + 557 == 2354 -checks_never_observed_red 1797 +# +# #1341, what expire refuses (OffgridwithJD): 1797 -> 1801. Seven new ttl_expire checks +# on tables whose every row is past retention, so a missing refusal drops rows. Three +# arms are dated by a mutation of expire: no owner check (a non-owner holding DELETE +# then retired all three groups), and a silent return 0 in place of the 42703 and of +# the 42804 refusal. Four arrive never: two premises on the fixture, set_options +# accepting an int retention column, and the owner's control, which has its own table +# so no refusal's mutation can reach it. Five-major runs on the files merged with +# #1353 and #1355: 77/77 on 15-19. +# +# ledger rows 2354 + 7 = 2361 +# never 1797 + 4 = 1801 +# dated 557 + 3 = 560 +# 1801 + 560 == 2361 +checks_never_observed_red 1801