From cd5fe6d6df0678a94ae0db74c98964cf85848eb1 Mon Sep 17 00:00:00 2001 From: "Joshua D. Drake" Date: Tue, 13 Oct 2026 15:48:02 +0000 Subject: [PATCH 1/4] test: a partition named with the drop block's dollar-quote tag must drop as planned, red (#1331 review) Red on PG17 at 614eb02: the DO block's body ends at the tag inside the quoted partition name, so the statement fails to parse and the partition stays. Not exploitable (jdatcmd's probe), but the plan does not run. Co-Authored-By: Claude Opus 5.5 --- test/partition_lifecycle.sh | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/test/partition_lifecycle.sh b/test/partition_lifecycle.sh index 4eb32c991..b31e72b30 100755 --- a/test/partition_lifecycle.sh +++ b/test/partition_lifecycle.sh @@ -372,6 +372,18 @@ states="$states;$(stmt_state "${df_plan[1]}")" check "after a DETACH that fails, the plan's DROP refuses and the partition stays attached" \ "$states|$(kids df)|$(q "SELECT count(*) FROM pg_inherits WHERE inhrelid = 'df_1'::regclass;")" "55P03;55000|2|1" +# A partition whose name holds the DO block's own dollar-quote tag (jdatcmd, #1331 +# review): the guarded drop must still be one statement that runs as returned. +q 'CREATE TABLE dq (ts timestamptz NOT NULL) PARTITION BY RANGE (ts); + CREATE TABLE "dq$pgcolumnar_drop$1" PARTITION OF dq FOR VALUES FROM ('"'2020-01-01 00:00+00'"') TO ('"'2020-02-01 00:00+00'"') USING pgcolumnar; + CREATE TABLE dq_2 PARTITION OF dq FOR VALUES FROM ('"'2020-02-01 00:00+00'"') TO ('"'2020-03-01 00:00+00'"') USING pgcolumnar;' >/dev/null +check "premise: a partition is named with the drop block's dollar-quote tag" \ + "$(q "SELECT count(*) FROM pg_class WHERE relname = 'dq\$pgcolumnar_drop\$1';")" "1" +dq_failures="$(run_plan dq '2020-02-01 00:00+00')" +check "a partition named with the drop block's dollar-quote tag is dropped by the plan as returned" \ + "${dq_failures:-every statement ran}|$(q "SELECT count(*) FROM pg_class WHERE relname = 'dq\$pgcolumnar_drop\$1';")|$(kids dq)" \ + "every statement ran|0|1" + # Refusals, as partition_create_ahead refuses. check "a drop plan for a parent with a DEFAULT partition is refused" \ "$(sqlstate_or "SELECT count(*) FROM pgcolumnar.partition_drop_plan('td', now())")" "55000" From 1c01762b98976d5fe3efba69f0356f6eeccdc703 Mon Sep 17 00:00:00 2001 From: "Joshua D. Drake" Date: Tue, 13 Oct 2026 15:49:06 +0000 Subject: [PATCH 2/4] fix: partition_drop_plan's drop block carries its body as a literal, not dollar-quoted (#1331 review) A partition name holding $pgcolumnar_drop$ ended the DO block's dollar quote and the statement failed to parse. The body is now format('DO %L', ...), so no name can end it. partition_lifecycle.sh 52/52 on PG17; identical patches in both scripts. Co-Authored-By: Claude Opus 5.5 --- pgcolumnar--1.0-alpha4--1.0-alpha5.sql | 20 ++++++++++++++------ pgcolumnar--1.0-alpha5.sql | 20 ++++++++++++++------ test/partition_lifecycle.sh | 8 +++++--- 3 files changed, 33 insertions(+), 15 deletions(-) diff --git a/pgcolumnar--1.0-alpha4--1.0-alpha5.sql b/pgcolumnar--1.0-alpha4--1.0-alpha5.sql index da7c58dd7..e4ef6d201 100644 --- a/pgcolumnar--1.0-alpha4--1.0-alpha5.sql +++ b/pgcolumnar--1.0-alpha4--1.0-alpha5.sql @@ -1068,9 +1068,13 @@ BEGIN LOOP RETURN NEXT format('ALTER TABLE %s DETACH PARTITION %s %s', parent, p.part, CASE WHEN p.pending THEN 'FINALIZE' ELSE 'CONCURRENTLY' END); - RETURN NEXT format('DO $pgcolumnar_drop$ BEGIN IF EXISTS (SELECT 1 FROM pg_catalog.pg_inherits WHERE inhrelid = %L::regclass) THEN RAISE EXCEPTION ''%% is still attached to its parent; its DETACH did not finish'', %L USING ERRCODE = ''55000''; END IF; DROP %s %s; END $pgcolumnar_drop$', - p.part::text, p.part::text, - CASE WHEN p.foreign_table THEN 'FOREIGN TABLE' ELSE 'TABLE' END, p.part); + -- the body as a literal, not dollar-quoted: no name can end it (#1331 review) + RETURN NEXT format('DO %L', + format('BEGIN IF EXISTS (SELECT 1 FROM pg_catalog.pg_inherits WHERE inhrelid = %L::regclass) THEN RAISE EXCEPTION %L, %L USING ERRCODE = %L; END IF; DROP %s %s; END', + p.part::text, + '% is still attached to its parent; its DETACH did not finish', + p.part::text, '55000', + CASE WHEN p.foreign_table THEN 'FOREIGN TABLE' ELSE 'TABLE' END, p.part)); END LOOP; END $drop_plan_tz$; @@ -1093,9 +1097,13 @@ BEGIN LOOP RETURN NEXT format('ALTER TABLE %s DETACH PARTITION %s %s', parent, p.part, CASE WHEN p.pending THEN 'FINALIZE' ELSE 'CONCURRENTLY' END); - RETURN NEXT format('DO $pgcolumnar_drop$ BEGIN IF EXISTS (SELECT 1 FROM pg_catalog.pg_inherits WHERE inhrelid = %L::regclass) THEN RAISE EXCEPTION ''%% is still attached to its parent; its DETACH did not finish'', %L USING ERRCODE = ''55000''; END IF; DROP %s %s; END $pgcolumnar_drop$', - p.part::text, p.part::text, - CASE WHEN p.foreign_table THEN 'FOREIGN TABLE' ELSE 'TABLE' END, p.part); + -- the body as a literal, not dollar-quoted: no name can end it (#1331 review) + RETURN NEXT format('DO %L', + format('BEGIN IF EXISTS (SELECT 1 FROM pg_catalog.pg_inherits WHERE inhrelid = %L::regclass) THEN RAISE EXCEPTION %L, %L USING ERRCODE = %L; END IF; DROP %s %s; END', + p.part::text, + '% is still attached to its parent; its DETACH did not finish', + p.part::text, '55000', + CASE WHEN p.foreign_table THEN 'FOREIGN TABLE' ELSE 'TABLE' END, p.part)); END LOOP; END $drop_plan_ts$; diff --git a/pgcolumnar--1.0-alpha5.sql b/pgcolumnar--1.0-alpha5.sql index 2d7b0b333..b697ee9cb 100644 --- a/pgcolumnar--1.0-alpha5.sql +++ b/pgcolumnar--1.0-alpha5.sql @@ -2312,9 +2312,13 @@ BEGIN LOOP RETURN NEXT format('ALTER TABLE %s DETACH PARTITION %s %s', parent, p.part, CASE WHEN p.pending THEN 'FINALIZE' ELSE 'CONCURRENTLY' END); - RETURN NEXT format('DO $pgcolumnar_drop$ BEGIN IF EXISTS (SELECT 1 FROM pg_catalog.pg_inherits WHERE inhrelid = %L::regclass) THEN RAISE EXCEPTION ''%% is still attached to its parent; its DETACH did not finish'', %L USING ERRCODE = ''55000''; END IF; DROP %s %s; END $pgcolumnar_drop$', - p.part::text, p.part::text, - CASE WHEN p.foreign_table THEN 'FOREIGN TABLE' ELSE 'TABLE' END, p.part); + -- the body as a literal, not dollar-quoted: no name can end it (#1331 review) + RETURN NEXT format('DO %L', + format('BEGIN IF EXISTS (SELECT 1 FROM pg_catalog.pg_inherits WHERE inhrelid = %L::regclass) THEN RAISE EXCEPTION %L, %L USING ERRCODE = %L; END IF; DROP %s %s; END', + p.part::text, + '% is still attached to its parent; its DETACH did not finish', + p.part::text, '55000', + CASE WHEN p.foreign_table THEN 'FOREIGN TABLE' ELSE 'TABLE' END, p.part)); END LOOP; END $drop_plan_tz$; @@ -2337,9 +2341,13 @@ BEGIN LOOP RETURN NEXT format('ALTER TABLE %s DETACH PARTITION %s %s', parent, p.part, CASE WHEN p.pending THEN 'FINALIZE' ELSE 'CONCURRENTLY' END); - RETURN NEXT format('DO $pgcolumnar_drop$ BEGIN IF EXISTS (SELECT 1 FROM pg_catalog.pg_inherits WHERE inhrelid = %L::regclass) THEN RAISE EXCEPTION ''%% is still attached to its parent; its DETACH did not finish'', %L USING ERRCODE = ''55000''; END IF; DROP %s %s; END $pgcolumnar_drop$', - p.part::text, p.part::text, - CASE WHEN p.foreign_table THEN 'FOREIGN TABLE' ELSE 'TABLE' END, p.part); + -- the body as a literal, not dollar-quoted: no name can end it (#1331 review) + RETURN NEXT format('DO %L', + format('BEGIN IF EXISTS (SELECT 1 FROM pg_catalog.pg_inherits WHERE inhrelid = %L::regclass) THEN RAISE EXCEPTION %L, %L USING ERRCODE = %L; END IF; DROP %s %s; END', + p.part::text, + '% is still attached to its parent; its DETACH did not finish', + p.part::text, '55000', + CASE WHEN p.foreign_table THEN 'FOREIGN TABLE' ELSE 'TABLE' END, p.part)); END LOOP; END $drop_plan_ts$; diff --git a/test/partition_lifecycle.sh b/test/partition_lifecycle.sh index b31e72b30..98dd1e007 100755 --- a/test/partition_lifecycle.sh +++ b/test/partition_lifecycle.sh @@ -204,10 +204,12 @@ plan() { q "SELECT string_agg(s, E'\n') FROM pgcolumnar.partition_drop_plan('$1' # The drop each partition gets: a DO block that refuses (55000) while the partition # is still attached, so a DETACH that failed cannot be followed by a DROP that takes # ACCESS EXCLUSIVE on the parent (jdatcmd, #1331 review). +sqlit() { local x="${1//\'/\'\'}"; printf "'%s'" "$x"; } # a SQL string literal gdrop() { # gdrop 'TABLE'|'FOREIGN TABLE' QUALIFIED-NAME -> the planned statement - local lit="'${2//\'/\'\'}'" - printf "DO \$pgcolumnar_drop\$ BEGIN IF EXISTS (SELECT 1 FROM pg_catalog.pg_inherits WHERE inhrelid = %s::regclass) THEN RAISE EXCEPTION '%% is still attached to its parent; its DETACH did not finish', %s USING ERRCODE = '55000'; END IF; DROP %s %s; END \$pgcolumnar_drop\$" \ - "$lit" "$lit" "$1" "$2" + # The body is a string literal, not dollar-quoted, so no name can end it. + local body + body="BEGIN IF EXISTS (SELECT 1 FROM pg_catalog.pg_inherits WHERE inhrelid = $(sqlit "$2")::regclass) THEN RAISE EXCEPTION '% is still attached to its parent; its DETACH did not finish', $(sqlit "$2") USING ERRCODE = '55000'; END IF; DROP $1 $2; END" + printf 'DO %s' "$(sqlit "$body")" } run_plan() { # each statement top-level, as \gexec would run it local line From 31867b8a84dfc81ebe9622137e8d35202882ca27 Mon Sep 17 00:00:00 2001 From: "Joshua D. Drake" Date: Tue, 13 Oct 2026 15:49:50 +0000 Subject: [PATCH 3/4] test: the twin's arm for a partition named with the drop block's dollar-quote tag (#1331 review) Red against main's dollar-quoted drop (a scratch copy of this file with main's plan text, so only the new arm can fail); green with the fix. compare_to_bash: nothing missing. Co-Authored-By: Claude Opus 5.5 --- test/pytest/TESTS.md | 2 +- test/pytest/test_partition_lifecycle.py | 29 ++++++++++++++++++++----- 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/test/pytest/TESTS.md b/test/pytest/TESTS.md index 07981df9d..c8c23a5e8 100644 --- a/test/pytest/TESTS.md +++ b/test/pytest/TESTS.md @@ -6549,7 +6549,7 @@ Removal proofs, each red in both twins: the prototype's zone handling; `CREATE T | test | what it holds | | --- | --- | | `test_partition_create_ahead` | from nothing it reaches now plus the lead, columnar, contiguous and one width wide; a second call creates nothing, and asking further ahead creates only the difference; rows land in columnar partitions; the parent is never held ACCESS EXCLUSIVE; west of UTC, for both key types, partitions continue a hand-made one contiguously and a row at every boundary routes; east of UTC, a timestamp parent covers the local now and starts on the width's boundary; inputs that broke the first version (a generated column, a 63-character name, more than 1000 partitions, a MAXVALUE bound) are handled or refused before any DDL; a DEFAULT partition is refused and nothing is created | -| `test_partition_drop_plan` | the drop half: `partition_drop_plan` lists `DETACH ... CONCURRENTLY` and `DROP` for the partitions wholly before the cutoff, in range order, from partitions created newest first. A cutoff on an upper bound includes the partition, and one before them all plans nothing. Running the plan, each statement top-level, leaves the rest with their rows and drops the storage rows. A `timestamp` key with a `timestamp` cutoff plans the same in Tokyo and Denver, and a cutoff of the other type is refused (`42804`). MINVALUE is listed once past, and MAXVALUE never. Quoted schema and partition names run as returned. A sub-partitioned partition is planned whole and dropped with its children. A tiered partition is planned with `DROP FOREIGN TABLE`, and its Parquet file stays. A detach cancelled in its second phase is planned as `FINALIZE` and dropped. The DEFAULT and non-timestamp parents are refused (`55000`) | +| `test_partition_drop_plan` | the drop half: `partition_drop_plan` lists `DETACH ... CONCURRENTLY` and `DROP` for the partitions wholly before the cutoff, in range order, from partitions created newest first. A cutoff on an upper bound includes the partition, and one before them all plans nothing. Running the plan, each statement top-level, leaves the rest with their rows and drops the storage rows. A `timestamp` key with a `timestamp` cutoff plans the same in Tokyo and Denver, and a cutoff of the other type is refused (`42804`). MINVALUE is listed once past, and MAXVALUE never. Quoted schema and partition names run as returned. A sub-partitioned partition is planned whole and dropped with its children. A tiered partition is planned with `DROP FOREIGN TABLE`, and its Parquet file stays. A partition whose name holds the drop block's dollar-quote tag drops as planned. A detach cancelled in its second phase is planned as `FINALIZE` and dropped. The DEFAULT and non-timestamp parents are refused (`55000`) | ## 94. test_approx_aggregates.py: hll and tdigest over columnar tables, the compatibility statement diff --git a/test/pytest/test_partition_lifecycle.py b/test/pytest/test_partition_lifecycle.py index d08503bcd..3c35fa462 100644 --- a/test/pytest/test_partition_lifecycle.py +++ b/test/pytest/test_partition_lifecycle.py @@ -263,12 +263,14 @@ def q(name): def gdrop(kind, name): """The drop the plan gives a partition: a DO block that refuses (55000) while it is still attached, so a failed DETACH is never followed by a DROP - that locks the parent (#1331 review).""" - lit = "'" + name.replace("'", "''") + "'" - return ("DO $pgcolumnar_drop$ BEGIN IF EXISTS (SELECT 1 FROM pg_catalog.pg_inherits" - f" WHERE inhrelid = {lit}::regclass) THEN RAISE EXCEPTION '% is still attached to its parent;" - f" its DETACH did not finish', {lit} USING ERRCODE = '55000'; END IF; DROP {kind} {name};" - " END $pgcolumnar_drop$") + that locks the parent (#1331 review). Its body is a string literal, not + dollar-quoted, so no partition name can end it.""" + def lit(x): + return "'" + x.replace("'", "''") + "'" + body = ("BEGIN IF EXISTS (SELECT 1 FROM pg_catalog.pg_inherits" + f" WHERE inhrelid = {lit(name)}::regclass) THEN RAISE EXCEPTION '% is still attached to its parent;" + f" its DETACH did not finish', {lit(name)} USING ERRCODE = '55000'; END IF; DROP {kind} {name}; END") + return "DO " + lit(body) def sqlstate_with(sql, setup=None): """'ok', or the SQLSTATE the statement failed with, in its own session.""" @@ -470,6 +472,21 @@ def sqlstate(sql): expect.text(f"{';'.join(states)}|{_kids(conn, 'xf')}|{attached}", "55P03;55000|2|1", "after a DETACH that fails, the plan's DROP refuses and the partition stays attached") + # A partition named with the drop block's own dollar-quote tag (#1331 review). + with conn.cursor() as cur: + cur.execute("CREATE TABLE xq (ts timestamptz NOT NULL) PARTITION BY RANGE (ts)") + cur.execute('CREATE TABLE "xq$pgcolumnar_drop$a" PARTITION OF xq' + " FOR VALUES FROM ('2019-10-01 00:00+00') TO ('2019-10-08 00:00+00') USING pgcolumnar") + cur.execute("CREATE TABLE xq_b PARTITION OF xq" + " FOR VALUES FROM ('2019-10-08 00:00+00') TO ('2019-10-15 00:00+00') USING pgcolumnar") + named = ("SELECT count(*) FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace" + f" WHERE n.nspname = '{schema}' AND c.relname = 'xq$pgcolumnar_drop$a'") + expect.num(_one(conn, named), 1, "premise: a partition is named with the drop block's dollar-quote tag") + failed = [st for st in plan("xq", "2019-10-08 00:00+00") if sqlstate_with(st) != "ok"] + expect.text(f"{'; '.join(failed) or 'every statement ran'}|{_one(conn, named)}|{_kids(conn, 'xq')}", + "every statement ran|0|1", + "a partition named with the drop block's dollar-quote tag is dropped by the plan as returned") + with conn.cursor() as cur: cur.execute("CREATE TABLE xd (ts timestamptz NOT NULL) PARTITION BY RANGE (ts)") cur.execute("CREATE TABLE xd_default PARTITION OF xd DEFAULT USING pgcolumnar") From 6dc5751f54d92dee68968355d2d27a58f830bed9 Mon Sep 17 00:00:00 2001 From: "Joshua D. Drake" Date: Tue, 13 Oct 2026 15:52:49 +0000 Subject: [PATCH 4/4] test: the dollar-quote arm in the ledger, dated by main's drop (#1331 review) Ledger 2311 / 1805, gate rc=0. Co-Authored-By: Claude Opus 5.5 --- test/check_ledger.tsv | 2 ++ test/check_ledger_budget.txt | 13 ++++++++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/test/check_ledger.tsv b/test/check_ledger.tsv index 53ea84c43..5c46d6220 100644 --- a/test/check_ledger.tsv +++ b/test/check_ledger.tsv @@ -1865,6 +1865,7 @@ partition_lifecycle partition_lifecycle a parent with a DEFAULT partition is ref partition_lifecycle partition_lifecycle a parent with a generated column gets its partitions, and the column stays generated 15;16;17;18;19 2026-10-12 partition_create_ahead stopping one width short of now plus the lead;partition_create_ahead without INCLUDING GENERATED partition_lifecycle partition_lifecycle a parent with a partition bounded by MAXVALUE is refused, and nothing is created 15;16;17;18;19 2026-10-12 partition_create_ahead without the MAXVALUE refusal partition_lifecycle partition_lifecycle a partition from MINVALUE is listed once its upper bound is past, one to MAXVALUE never 15;16;17;18;19 2026-10-13 a partition planned only when it has a lower bound, so never one from MINVALUE;a partition whose upper bound equals the cutoff left out;a plain DROP, not refusing while the partition is still attached;the cutoff filter removed: every partition planned;the lower bound read where the upper is meant;the plan detaches but drops nothing +partition_lifecycle partition_lifecycle a partition named with the drop block's dollar-quote tag is dropped by the plan as returned 15;16;17;18;19 2026-10-13 the drop block's body dollar-quoted, as on main at 614eb02 partition_lifecycle partition_lifecycle a partition that is itself partitioned is planned whole 15;16;17;18;19 2026-10-13 a partition that is itself partitioned left out of the plan;a partition whose upper bound equals the cutoff left out;a plain DROP, not refusing while the partition is still attached;the cutoff filter removed: every partition planned;the lower bound read where the upper is meant;the plan detaches but drops nothing;the plan detaches without CONCURRENTLY partition_lifecycle partition_lifecycle a pending detach is finalised, not started again 15;16;17;18;19 2026-10-13 a partition whose upper bound equals the cutoff left out;a pending detach planned as a new concurrent detach, not FINALIZE;a plain DROP, not refusing while the partition is still attached;the cutoff filter removed: every partition planned;the lower bound read where the upper is meant;the plan detaches but drops nothing partition_lifecycle partition_lifecycle a plan for names that need quoting runs as returned 15;16;17;18;19 2026-10-13 a partition whose upper bound equals the cutoff left out;names in the plan with their quotes removed;the cutoff filter removed: every partition planned;the lower bound read where the upper is meant;the plan detaches but drops nothing @@ -1891,6 +1892,7 @@ partition_lifecycle partition_lifecycle asking further ahead creates only the di partition_lifecycle partition_lifecycle every partition it creates is columnar 15;16;17;18;19 2026-10-12 partition_create_ahead creating heap partitions partition_lifecycle partition_lifecycle from no partitions it creates enough to reach now plus the lead 15;16;17;18;19 2026-10-12 partition_create_ahead stopping one width short of now plus the lead partition_lifecycle partition_lifecycle it never takes AccessExclusiveLock on the parent 15;16;17;18;19 2026-10-12 partition_create_ahead using CREATE TABLE ... PARTITION OF +partition_lifecycle partition_lifecycle premise: a partition is named with the drop block's dollar-quote tag 15;16;17;18;19 never - partition_lifecycle partition_lifecycle premise: a timestamp parent continued from a hand-made partition, in a session west of UTC 15;16;17;18;19 never - partition_lifecycle partition_lifecycle premise: a timestamp parent was given partitions from a session east of UTC 15;16;17;18;19 never - partition_lifecycle partition_lifecycle premise: a timestamptz parent continued from a hand-made partition, in a session west of UTC 15;16;17;18;19 never - diff --git a/test/check_ledger_budget.txt b/test/check_ledger_budget.txt index 3649f2b7a..d568a5da9 100644 --- a/test/check_ledger_budget.txt +++ b/test/check_ledger_budget.txt @@ -1625,4 +1625,15 @@ suites_not_covered 246 # never 1801 + 3 = 1804 # dated 485 + 20 = 505 # 1804 + 505 == 2309 -checks_never_observed_red 1804 +# +# The drop block's body as a literal (#1331 review, OffgridwithJD): 1804 -> 1805. +# Two checks from a five-major run (partition_lifecycle.sh 52/52 on each of +# 15-19): the arm, a partition named with the block's dollar-quote tag dropped +# as planned, dated by the run on main's dollar-quoted drop; and its premise, +# that the fixture's name holds the tag, which arrives never. +# +# ledger rows 2309 + 2 = 2311 +# never 1804 + 1 = 1805 +# dated 505 + 1 = 506 +# 1805 + 506 == 2311 +checks_never_observed_red 1805