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/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 diff --git a/test/partition_lifecycle.sh b/test/partition_lifecycle.sh index 4eb32c991..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 @@ -372,6 +374,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" 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")