diff --git a/test/check_ledger.tsv b/test/check_ledger.tsv index 4ac845e2d..a975ef2d2 100644 --- a/test/check_ledger.tsv +++ b/test/check_ledger.tsv @@ -2256,7 +2256,7 @@ ttl_expire ttl_expire a negative ttl_interval is refused with 22023 15;16;17;18; 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 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 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 ttl_expire ttl_expire and VACUUM really did set visibility-map bits to clear 15;16;17;18;19 never - ttl_expire ttl_expire and a zero ttl_interval is refused too 15;16;17;18;19 never - ttl_expire ttl_expire and its rows are gone 15;16;17;18;19 never - @@ -2269,6 +2269,7 @@ ttl_expire ttl_expire and the table really is smaller than it was 15;16;17;18;19 ttl_expire ttl_expire and they are the groups gone from the row-group catalog 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 and those NULL rows are still there 15;16;17;18;19 never - ttl_expire ttl_expire but the zone map still records the deleted NULLs 15;16;17;18;19 never - +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 the rows are all still there 15;16;17;18;19 never - @@ -2292,7 +2293,6 @@ 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: compact_rewrite retired a group 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 - diff --git a/test/check_ledger_budget.txt b/test/check_ledger_budget.txt index 589b6be08..cf3fcaf7f 100644 --- a/test/check_ledger_budget.txt +++ b/test/check_ledger_budget.txt @@ -1818,4 +1818,18 @@ suites_not_covered 246 # never 1801 + 2 - 2 - 3 = 1798 # dated 539 + 1 + 2 + 3 = 545 # 1798 + 545 == 2343 -checks_never_observed_red 1798 +# +# #1339 audit, ttl_expire's twins (OffgridwithJD): 1798 -> 1796. Two arms gain their +# first red date. The no-retention arm now requires SQLSTATE 55000, as its pytest twin +# does (it accepted any ERROR); it is red with the refusal raised as XX000. And +# "compact_rewrite retired a group" tests product code, so it loses the `premise:` +# prefix (#1236), its row renamed with its history, and is red with compact_rewrite +# returning 0. The pytest date arm now reads expire's own return, as the shell twin +# does; that is a pytest change and moves no row. Five-major runs on the tree merged +# with #1353: 70/70 on 15-19. +# +# ledger rows 2343 = 2343 +# never 1798 - 2 = 1796 +# dated 545 + 2 = 547 +# 1796 + 547 == 2343 +checks_never_observed_red 1796 diff --git a/test/pytest/test_ttl_expire.py b/test/pytest/test_ttl_expire.py index dc23fcd7a..5e3c2d5a1 100644 --- a/test/pytest/test_ttl_expire.py +++ b/test/pytest/test_ttl_expire.py @@ -502,22 +502,27 @@ def test_a_date_retention_column_truncates_toward_keeping(pgc_conn, expect): on_cutoff, 1, "premise: and rows dated exactly ON the cutoff, which decide the rounding") - state = _sqlstate(pgc_conn, "SELECT pgcolumnar.expire('ttl_dt')") + # THE CALL'S OWN ANSWER, AS THE SHELL TWIN READS IT (#1339). This used to discard + # it and call expire a second time, so "retires at least one group" was decided + # by the row count alone, and an expire that retired groups but reported 0 passed. + try: + first = _one(pgc_conn, "SELECT pgcolumnar.expire('ttl_dt')") + state = "" + except psycopg.Error as exc: + first, state = None, exc.sqlstate or "?" expect.text( "accepted" if state == "" else f"refused ({state})", "accepted", "a date retention column is accepted, not refused (#1135)") - # Re-read after the call above, which ran it once. - retired = _one(pgc_conn, "SELECT pgcolumnar.expire('ttl_dt')") rows_after = _one(pgc_conn, "SELECT count(*) FROM ttl_dt") live_after = _one(pgc_conn, f"SELECT count(*) FROM ttl_dt WHERE d >= {cutoff}") print(f"-- date: {rows_before} rows in {groups_before} groups, " f"{d_expired} past retention, {on_cutoff} on the cutoff") - print(f"-- date: rows left {rows_after}, second call retired {retired}") + print(f"-- date: expire returned {first}, rows left {rows_after}") expect.text( - "retired" if rows_after < rows_before else f"RETIRED NOTHING ({rows_after})", + "retired" if (first or 0) > 0 else f"RETIRED NOTHING ({first})", "retired", "expire on a date column retires at least one group") expect.num( @@ -697,7 +702,7 @@ def rec(sid): cur.execute("DELETE FROM ttl_rr WHERE v % 10 < 6") cur.execute("SELECT pgcolumnar.compact_rewrite('ttl_rr', 0.1)") expect.text("retired" if rgs(sid) != "4,5" else "nothing retired [4,5]", "retired", - "premise: compact_rewrite retired a group") + "compact_rewrite retired a group") expect.text(rec(sid), "1,2,3", "a compaction's retired groups are not recorded as retention") pgc_conn.execute("DROP TABLE ttl_rr") diff --git a/test/ttl_expire.sh b/test/ttl_expire.sh index 3f16081e7..3ebd1c875 100755 --- a/test/ttl_expire.sh +++ b/test/ttl_expire.sh @@ -131,12 +131,23 @@ check "every remaining row reads back its own value" \ AGAIN="$(q "SELECT pgcolumnar.expire('ttl_t')")" check "running it again retires nothing, because nothing new expired" "$AGAIN" "0" +# SQLSTATE, not message text: the refusals below each have their own state, while a +# missing function would be 42883 and a non-owner 42501. Empty when the call succeeds. +ttl_state() { + env PATH="$PGC_BINDIR:$PATH" psql -h 127.0.0.1 -p "$PGC_PORT" -U postgres \ + -d "$PGC_DB" -v ON_ERROR_STOP=1 -Atq -v VERBOSITY=sqlstate -c "$1" 2>&1 \ + | sed -n 's/^ERROR: \([0-9A-Z]\{5\}\).*/\1/p' | head -1 +} + # ---- a table with no declared retention ------------------------------------ psql_run "CREATE TABLE ttl_none (id int, ts timestamptz) USING pgcolumnar;" psql_run "INSERT INTO ttl_none SELECT g, now() FROM generate_series(1,10) g;" -ERR="$(q "SELECT pgcolumnar.expire('ttl_none')")" +# BY SQLSTATE, AS THE PYTEST TWIN ASSERTS (#1339). The message grep accepted any +# ERROR at all -- a missing function, a non-owner, an internal error -- so a refusal +# raised as the wrong kind of failure passed. 55000 is the prerequisite-state check. +_none_state="$(ttl_state "SELECT pgcolumnar.expire('ttl_none')")" check "a table with no declared retention is an error, not a silent success" \ - "$(grep -qiE 'ERROR|no retention|ttl' <<<"$ERR" && echo "refused" || echo "ACCEPTED ($ERR)")" "refused" + "${_none_state:-ACCEPTED}" "55000" # ---- NULL in the retention column is a straddle, not an expiry ------------ psql_run "CREATE TABLE ttl_null (id int, ts timestamptz, v text) USING pgcolumnar;" @@ -235,13 +246,6 @@ check "index-only scan does not return rows expire already retired" "$IOS_AFTER" # A negative ttl_interval puts the cutoff in the FUTURE, so expire finds # `maximum < cutoff` true for groups entirely inside their retention and retires # them: live rows dropped, which is the failure this suite is named for. -# SQLSTATE, not message text -- 22023 comes from the range check, while a -# missing function would be 42883 and a non-owner 42501. -ttl_state() { - env PATH="$PGC_BINDIR:$PATH" psql -h 127.0.0.1 -p "$PGC_PORT" -U postgres \ - -d "$PGC_DB" -v ON_ERROR_STOP=1 -Atq -v VERBOSITY=sqlstate -c "$1" 2>&1 \ - | sed -n 's/^ERROR: \([0-9A-Z]\{5\}\).*/\1/p' | head -1 -} psql_run "CREATE TABLE ttl_neg (id int, ts timestamptz) USING pgcolumnar;" psql_run "INSERT INTO ttl_neg SELECT g, now() FROM generate_series(1,100) g;" check "a negative ttl_interval is refused with 22023" \ @@ -550,7 +554,7 @@ check "and they are the groups gone from the row-group catalog" "$(_rgs)" "4" psql_run "INSERT INTO ttl_rec SELECT now() + g * interval '1 second', g FROM generate_series(1001, 3000) g; DELETE FROM ttl_rec WHERE v % 10 < 6 AND ts > now(); SELECT pgcolumnar.compact_rewrite('ttl_rec', 0.1);" -check "premise: compact_rewrite retired a group" \ +check "compact_rewrite retired a group" \ "$([ "$(_rgs)" != "4,5,6" ] && echo retired || echo "nothing retired [$(_rgs)]")" "retired" check "a compaction's retired groups are not recorded as retention" "$(_rec)" "1,2,3"