Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions docs/limitations.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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

Expand Down
19 changes: 19 additions & 0 deletions test/am_contracts.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
4 changes: 4 additions & 0 deletions test/check_ledger.tsv
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 11 additions & 0 deletions test/check_ledger_budget.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
1 change: 1 addition & 0 deletions test/pytest/TESTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
3 changes: 2 additions & 1 deletion test/pytest/expected_tests.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
50 changes: 50 additions & 0 deletions test/pytest/test_am_contracts.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Loading