From f32555b58c70913b2db01a2ef754d77e17042fba Mon Sep 17 00:00:00 2001 From: "Joshua D. Drake" Date: Thu, 15 Oct 2026 01:25:36 +0000 Subject: [PATCH] test: the documented refusals change nothing (#1341 items 44, 51, 77) CREATE UNLOGGED TABLE and ALTER TABLE ... SET UNLOGGED are refused (0A000) and leave nothing changed; ALTER TABLE ... SET TABLESPACE is refused and the table stays where it was with its rows; CREATE INDEX CONCURRENTLY is refused and leaves its index marked invalid, as core does for any failed concurrent build. Nothing tested any of them. Arms in both twins of am_contracts, each red under its own mutation. limitations.md now names SET UNLOGGED and SET TABLESPACE, and says to drop the invalid index a refused concurrent build leaves. cluster_tests +1. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_019VvWuX1zUwk6Y513waRd4N --- docs/limitations.md | 9 ++++-- test/am_contracts.sh | 19 ++++++++++++ test/check_ledger.tsv | 4 +++ test/check_ledger_budget.txt | 11 +++++++ test/pytest/TESTS.md | 1 + test/pytest/expected_tests.txt | 3 +- test/pytest/test_am_contracts.py | 50 ++++++++++++++++++++++++++++++++ 7 files changed, 94 insertions(+), 3 deletions(-) diff --git a/docs/limitations.md b/docs/limitations.md index edcaa84cd..b6b822d39 100644 --- a/docs/limitations.md +++ b/docs/limitations.md @@ -657,7 +657,8 @@ other than where the feature is used: `ON CONFLICT DO NOTHING` takes no row lock and works, with or without a conflict target. -`CREATE TABLE` refuses an unlogged columnar table for the same reason. Both cases +`CREATE TABLE` refuses an unlogged columnar table for the same reason, and so does +`ALTER TABLE ... SET UNLOGGED`. Both cases follow one rule. This access method refuses a configuration that it cannot support at the point where you choose it. It does not refuse it at each later use. @@ -783,7 +784,11 @@ therefore still be cheaper as a scan. of a deleted row only while a transaction that began before the delete is still running, as it does on heap. - `CREATE INDEX CONCURRENTLY` (the concurrent validate path) and partial - block-range index builds are not supported. + block-range index builds are not supported. A refused `CREATE INDEX + CONCURRENTLY` leaves its index behind, marked invalid, as PostgreSQL does for any + concurrent build that fails. Drop that index before you build it again. +- `ALTER TABLE ... SET TABLESPACE` is not supported on a columnar table. It is + refused, and the table stays where it was. ## Constraints on the import path diff --git a/test/am_contracts.sh b/test/am_contracts.sh index 2a7c38bad..1f0d82da7 100755 --- a/test/am_contracts.sh +++ b/test/am_contracts.sh @@ -351,4 +351,23 @@ check "REINDEX after a committed delete of most rows reclaims their entries, as "$(ix_reclaim pgcolumnar)|$(ix_reclaim heap)|$(q "SET enable_seqscan = off; SELECT count(*) FROM ix_pgcolumnar WHERE a > 0;" | tail -1)" \ "reclaimed|reclaimed|10000" +# ---- documented refusals (#1341 items 44, 51, 77) ---------------------------------- +# +# Each is refused where it is chosen, as limitations.md says, and refused cleanly: +# nothing is left changed by the attempt. +rf_count() { q "SELECT count(*) || '|' || sum(a) FROM rf"; } +psql_run "CREATE TABLE rf (a int) USING pgcolumnar; INSERT INTO rf SELECT g FROM generate_series(1, 5000) g;" +check "CREATE UNLOGGED TABLE ... USING pgcolumnar is refused (0A000) and leaves no table" \ + "$(state "CREATE UNLOGGED TABLE rf_u (a int) USING pgcolumnar")|$(q "SELECT count(*) FROM pg_class WHERE relname = 'rf_u'")" "0A000|0" +check "ALTER TABLE ... SET UNLOGGED is refused (0A000), and the table stays logged with its rows" \ + "$(state "ALTER TABLE rf SET UNLOGGED")|$(q "SELECT relpersistence FROM pg_class WHERE relname = 'rf'")|$(rf_count)" "0A000|p|5000|12502500" +pgc_pg "mkdir -p -m 700 '$PGC_WORKDIR/rf_ts'" +psql_run "CREATE TABLESPACE rf_ts LOCATION '$PGC_WORKDIR/rf_ts';" +check "ALTER TABLE ... SET TABLESPACE is refused (0A000), and the table stays where it was with its rows" \ + "$(state "ALTER TABLE rf SET TABLESPACE rf_ts")|$(q "SELECT reltablespace FROM pg_class WHERE relname = 'rf'")|$(rf_count)" "0A000|0|5000|12502500" +# A failed concurrent build leaves its index behind, marked invalid, as core does for +# any CREATE INDEX CONCURRENTLY that fails after its first phase. +check "CREATE INDEX CONCURRENTLY is refused (0A000), and the index it leaves is marked invalid" \ + "$(state "CREATE INDEX CONCURRENTLY rf_cic ON rf (a)")|$(q "SELECT indisvalid FROM pg_index WHERE indexrelid = 'rf_cic'::regclass")" "0A000|f" + pgc_summary diff --git a/test/check_ledger.tsv b/test/check_ledger.tsv index dda5e4fa5..4c98ca41b 100644 --- a/test/check_ledger.tsv +++ b/test/check_ledger.tsv @@ -13,6 +13,10 @@ alter_table_scans alter_table_scans and the rewrite kept the live rows and not t alter_table_scans alter_table_scans the deleted rows are gone from both tables, which hold the same rows 15;16;17;18;19 2026-10-15 every scan returns deleted rows, the reader's delete filter disabled (src/columnar_reader.c) alter_table_scans alter_table_scans the lookup of id 1 is an index scan on the primary key 15;16;17;18;19 2026-10-15 the index build marks every row alive, deleted ones included (src/columnar_tableam.c) alter_table_scans alter_table_scans the primary key still finds id 1 once, through the index, after both rewrites 15;16;17;18;19 never - +am_contracts am_contracts ALTER TABLE ... SET TABLESPACE is refused (0A000), and the table stays where it was with its rows 15;16;17;18;19 2026-10-15 relation copy returning without copying (SET TABLESPACE) +am_contracts am_contracts ALTER TABLE ... SET UNLOGGED is refused (0A000), and the table stays logged with its rows 15;16;17;18;19 2026-10-15 the storage hook accepting an unlogged columnar table +am_contracts am_contracts CREATE INDEX CONCURRENTLY is refused (0A000), and the index it leaves is marked invalid 15;16;17;18;19 2026-10-15 the concurrent index validate scan doing nothing +am_contracts am_contracts CREATE UNLOGGED TABLE ... USING pgcolumnar is refused (0A000) and leaves no table 15;16;17;18;19 2026-10-15 the storage hook accepting an unlogged columnar table am_contracts am_contracts ON CONFLICT DO NOTHING skips the duplicate and inserts the new row 15;16;17;18;19 2026-10-14 speculative insert refused as unsupported, as before #1341 am_contracts am_contracts ON CONFLICT DO UPDATE is refused as unsupported (0A000), and changes nothing 15;16;17;18;19 2026-10-14 speculative insert refused as unsupported, as before #1341 am_contracts am_contracts ON CONFLICT DO UPDATE with no conflicting row inserts it 15;16;17;18;19 2026-10-14 speculative insert refused as unsupported, as before #1341 diff --git a/test/check_ledger_budget.txt b/test/check_ledger_budget.txt index da01b2ca4..b17a87c23 100644 --- a/test/check_ledger_budget.txt +++ b/test/check_ledger_budget.txt @@ -2414,4 +2414,15 @@ suites_not_covered 231 # never 2282 + 0 = 2282 # dated 726 + 9 = 735 # 2282 + 735 == 3017 +# +# #1341 items 44, 51 and 77, the documented refusals (jdatcmd): 2282 -> 2282. +# am_contracts gains four checks from five-major runs (31/31 on each of 15-19), all +# dated: CREATE UNLOGGED and SET UNLOGGED by the storage hook accepting an unlogged +# table, CREATE INDEX CONCURRENTLY by its validate scan doing nothing, and SET +# TABLESPACE by the relation copy returning without copying. +# +# ledger rows 3017 + 4 = 3021 +# never 2282 + 0 = 2282 +# dated 735 + 4 = 739 +# 2282 + 739 == 3021 checks_never_observed_red 2282 diff --git a/test/pytest/TESTS.md b/test/pytest/TESTS.md index 97d277b4a..6659cd10a 100644 --- a/test/pytest/TESTS.md +++ b/test/pytest/TESTS.md @@ -6925,6 +6925,7 @@ THE WITHDRAWAL IS REACHED ONLY BY A RACE. When one session holds a key uncommitt | `test_an_insert_racing_an_uncommitted_delete_of_its_key` | an insert of a key whose delete (or key-changing update) is uncommitted waits, then fails with 23505 if the delete rolls back and succeeds if it commits: never two rows with the key. Before #1341's fix the rollback left two | | `test_an_index_built_after_a_delete_serves_an_older_snapshot` | a REPEATABLE READ snapshot from before a committed DELETE still finds the row through an index built after it (an Index Scan, asserted), and a new snapshot does not; a UNIQUE index still builds over a key whose other holder is deleted. Before #1341's fix the old snapshot's lookup found 0 | | `test_reindex_reclaims_the_entries_of_rows_no_snapshot_sees` | after a committed delete of 9 rows in 10, with no older snapshot, `REINDEX` takes the index under half its size, as it does on heap, and the index still finds the 10,000 live rows (#1407) | +| `test_the_documented_refusals_change_nothing` | `CREATE UNLOGGED TABLE` and `SET UNLOGGED` are refused (`0A000`) and leave nothing changed; `SET TABLESPACE` is refused and the table stays where it was; `CREATE INDEX CONCURRENTLY` is refused and leaves its index marked invalid, as core does (#1341 items 44, 51, 77) | ## 111. test_objstore_credentials.py: object-store credentials live on a user mapping, and the environment is a privilege diff --git a/test/pytest/expected_tests.txt b/test/pytest/expected_tests.txt index 316bd78ae..b84f2d3de 100644 --- a/test/pytest/expected_tests.txt +++ b/test/pytest/expected_tests.txt @@ -689,4 +689,5 @@ guard_tests 435 # test_rollup.py, by collection over the cluster files on this tree. # cluster_tests 577 -> 578 for #1341's test_statement_self_visibility.py, one test, by collection # over the cluster files on this tree. -cluster_tests 578 +# cluster_tests 578 -> 579 for #1341's test_the_documented_refusals_change_nothing in +cluster_tests 579 diff --git a/test/pytest/test_am_contracts.py b/test/pytest/test_am_contracts.py index 5780897d5..7e373c8d2 100644 --- a/test/pytest/test_am_contracts.py +++ b/test/pytest/test_am_contracts.py @@ -448,3 +448,53 @@ def reclaim(am): cur.execute("RESET enable_seqscan") expect.text(f"{col}|{heap}|{found}", "reclaimed|reclaimed|10000", "REINDEX after a committed delete of most rows reclaims their entries, as heap's does") + + +def test_the_documented_refusals_change_nothing(pgc_conn, expect): + """Each is refused where it is chosen, as limitations.md says, and nothing is left + changed by the attempt (#1341 items 44, 51, 77). A failed concurrent build leaves + its index behind, marked invalid, as core does for any such failure.""" + import os + import shutil + import tempfile + + def state(sql): + try: + with pgc_conn.cursor() as cur: + cur.execute(sql) + return "ok" + except psycopg.Error as e: + return e.sqlstate or "?" + + count = "SELECT count(*) || '|' || sum(a) FROM rf" + with pgc_conn.cursor() as cur: + cur.execute("CREATE TABLE rf (a int) USING pgcolumnar") + cur.execute("INSERT INTO rf SELECT g FROM generate_series(1, 5000) g") + expect.text(f"{state('CREATE UNLOGGED TABLE rf_u (a int) USING pgcolumnar')}|" + f"{_one(pgc_conn, 'SELECT count(*) FROM pg_class WHERE relname = ' + repr('rf_u'))}", + "0A000|0", "CREATE UNLOGGED TABLE ... USING pgcolumnar is refused (0A000) and leaves no table") + expect.text(f"{state('ALTER TABLE rf SET UNLOGGED')}|" + f"{_one(pgc_conn, 'SELECT relpersistence FROM pg_class WHERE relname = ' + repr('rf'))}|" + f"{_one(pgc_conn, count)}", + "0A000|p|5000|12502500", "ALTER TABLE ... SET UNLOGGED is refused (0A000), and the table stays logged with its rows") + + # A tablespace directory the server's own user owns, outside any root-only parent. + ts = tempfile.mkdtemp(prefix="pgc-rf-ts-", dir="/tmp") + os.chmod(ts, 0o700) + if os.geteuid() == 0: + shutil.chown(ts, user="postgres") + try: + with pgc_conn.cursor() as cur: + cur.execute(f"CREATE TABLESPACE rf_ts_py LOCATION '{ts}'") + expect.text(f"{state('ALTER TABLE rf SET TABLESPACE rf_ts_py')}|" + f"{_one(pgc_conn, 'SELECT reltablespace FROM pg_class WHERE relname = ' + repr('rf'))}|" + f"{_one(pgc_conn, count)}", + "0A000|0|5000|12502500", "ALTER TABLE ... SET TABLESPACE is refused (0A000), and the table stays where it was with its rows") + finally: + with pgc_conn.cursor() as cur: + cur.execute("DROP TABLESPACE IF EXISTS rf_ts_py") + shutil.rmtree(ts, ignore_errors=True) + + expect.text(f"{state('CREATE INDEX CONCURRENTLY rf_cic ON rf (a)')}|" + f"{_one(pgc_conn, 'SELECT indisvalid FROM pg_index WHERE indexrelid = ' + repr('rf_cic') + '::regclass')}", + "0A000|False", "CREATE INDEX CONCURRENTLY is refused (0A000), and the index it leaves is marked invalid")