From d9afe4c0c3cdad827e62778177cf7b7ec15091f0 Mon Sep 17 00:00:00 2001 From: "Joshua D. Drake" Date: Tue, 4 Aug 2026 20:33:53 -0600 Subject: [PATCH] test: pin PostgreSQL 19 parallel autovacuum on a columnar table (#398) The question was whether a table access method participates in 19's parallel autovacuum at all. Measured: the per-table storage parameter autovacuum_parallel_workers is ACCEPTED on a columnar table and lands in pg_class.reloptions exactly as it does on heap, and it has no effect. pgcolumnar_relation_vacuum marks the visibility map and retires fully-deleted row groups. It does no parallel work and never reads the parameter, so a user can set it and nothing happens. It cannot simply be rejected, which was my stated preference on the issue. Storage-parameter validation belongs to PostgreSQL and is driven by relkind rather than by the access method, and pgColumnar has no reloptions hook to refuse it from. Documenting it is the honest option that is actually available. test/pg19_vacuum_options.sh, registered, gated to 19 with a visible skip below it, asserts all of that: the parameter is accepted on both access methods and recorded in reloptions, VACUUM and VACUUM (PARALLEL 2) and pgcolumnar.vacuum all succeed with it set, and rows and content hash are unchanged by any of them. The heap arm is a control rather than decoration. autovacuum_max_parallel_workers defaults to 0 on 19beta2, so without a table where parallel vacuum demonstrably does something, "the parameter does nothing here" cannot be told apart from "parallel autovacuum does nothing on this cluster". The control asserts VACUUM VERBOSE reports on heap while the columnar path is silent, because the AM path is ours. If a future release makes table AMs participate, that check flips and should be rewritten to assert we report workers too. docs/limitations.md says plainly that the parameter is accepted, does nothing, and cannot be refused. Gate: five-major matrix, ALL VERSIONS PASSED, pg19_vacuum_options PASS on all five. Co-Authored-By: Claude Opus 5 (1M context) --- docs/limitations.md | 7 +++ test/pg19_vacuum_options.sh | 93 +++++++++++++++++++++++++++++++++++++ test/run_all_versions.sh | 2 +- 3 files changed, 101 insertions(+), 1 deletion(-) create mode 100755 test/pg19_vacuum_options.sh diff --git a/docs/limitations.md b/docs/limitations.md index 0d73153..e595a56 100644 --- a/docs/limitations.md +++ b/docs/limitations.md @@ -268,6 +268,13 @@ returning no rows. ## Vacuum and compaction +- `autovacuum_parallel_workers`, the per-table storage parameter PostgreSQL 19 adds + for parallel autovacuum, is accepted on a columnar table and has no effect. + pgColumnar implements its own vacuum, which marks the visibility map and retires + fully-deleted row groups, and does no parallel work. The parameter cannot be + refused: storage-parameter validation belongs to PostgreSQL and is driven by the + relation kind rather than by the access method. Setting it is harmless and + pointless. - `VACUUM FULL` and `CLUSTER` are not supported on a columnar table; the copy-for-cluster path raises an error. Use `pgcolumnar.vacuum` or `pgcolumnar.vacuum_full` instead. diff --git a/test/pg19_vacuum_options.sh b/test/pg19_vacuum_options.sh new file mode 100755 index 0000000..c008e44 --- /dev/null +++ b/test/pg19_vacuum_options.sh @@ -0,0 +1,93 @@ +#!/usr/bin/env bash +# +# PostgreSQL 19 parallel autovacuum, on a columnar table (issue #398). +# +# 19 adds parallel autovacuum: a server variable autovacuum_max_parallel_workers +# and a per-table storage parameter autovacuum_parallel_workers. pgColumnar +# implements its own relation_vacuum callback, so the question was whether a table +# access method participates in that at all. +# +# Measured answer: the storage parameter is ACCEPTED on a columnar table and lands +# in pg_class.reloptions exactly as it does on heap, and it has NO EFFECT, because +# pgcolumnar_relation_vacuum marks the visibility map and retires fully-deleted +# row groups and does no parallel work of any kind. A parameter a user can set and +# that silently does nothing is worth pinning, so it is recorded here and in +# docs/limitations.md rather than left to be discovered. +# +# It cannot simply be rejected. Storage-parameter validation is core's, driven by +# relkind rather than by the access method, and pgColumnar has no reloptions hook +# to refuse it from. Documenting it is the available honest option. +# +# The heap arm is a control rather than decoration: without it, "the parameter does +# nothing here" cannot be told apart from "parallel autovacuum does nothing on this +# cluster", and autovacuum_max_parallel_workers defaults to 0 on 19beta2, which +# would make the second reading true for everyone. +# +# Usage: test/pg19_vacuum_options.sh [PG_CONFIG] +# Written fresh for pgColumnar. + +set -uo pipefail +. "$(dirname "${BASH_SOURCE[0]}")/lib.sh" + +pgc_setup "${1:-/usr/local/pg19/bin/pg_config}" + +srv="$(q 'SHOW server_version_num')" +if [ "${srv:-0}" -lt 190000 ]; then + echo "SKIP parallel autovacuum requires PostgreSQL 19 (server_version_num=$srv)" + echo "pg19_vacuum_options.sh: SKIPPED" + exit 0 +fi + +ROWS=${PGC_AV_ROWS:-100000} + +psql_run "CREATE TABLE av_c (id int, v text) USING pgcolumnar; + CREATE TABLE av_h (id int, v text); + INSERT INTO av_c SELECT g,'x'||g FROM generate_series(1,$ROWS) g; + INSERT INTO av_h SELECT g,'x'||g FROM generate_series(1,$ROWS) g; + DELETE FROM av_c WHERE id % 4 = 0; + DELETE FROM av_h WHERE id % 4 = 0;" + +# ---- the setting exists, and is off by default ------------------------------- +check "autovacuum_max_parallel_workers exists on this server" \ + "$(q 'SHOW autovacuum_max_parallel_workers' >/dev/null 2>&1 && echo yes || echo no)" "yes" + +# ---- the storage parameter is accepted on BOTH access methods ---------------- +# This is the answer to the issue: a table AM does not get to refuse it. +for t in av_c av_h; do + out="$(psql_run "ALTER TABLE $t SET (autovacuum_parallel_workers = 3);" 2>&1)" + check "autovacuum_parallel_workers is accepted on $t" \ + "$(case "$out" in *ERROR*) grep -oE 'ERROR:.*' <<<"$out" | head -1 ;; *) echo accepted ;; esac)" \ + "accepted" + check "and it is recorded in reloptions for $t" \ + "$(q "SELECT array_to_string(reloptions,',') FROM pg_class WHERE relname='$t'")" \ + "autovacuum_parallel_workers=3" +done + +# ---- and it changes nothing about what our vacuum does ----------------------- +# Same live rows before and after, with the parameter set. The point is not that +# vacuum works; it is that setting the parameter neither helps nor breaks it. +live_before="$(q 'SELECT count(*) FROM av_c')" +hash_before="$(q "SELECT md5(string_agg(id::text||':'||v, ',' ORDER BY id)) FROM av_c")" + +check "VACUUM on a columnar table with the parameter set succeeds" \ + "$(psql_run 'VACUUM av_c;' 2>&1 | grep -c 'ERROR' || true)" "0" +check "VACUUM (PARALLEL 2) on a columnar table succeeds" \ + "$(psql_run 'VACUUM (PARALLEL 2) av_c;' 2>&1 | grep -c 'ERROR' || true)" "0" +check "pgcolumnar.vacuum with the parameter set succeeds" \ + "$(psql_run "SELECT pgcolumnar.vacuum('av_c');" 2>&1 | grep -c 'ERROR' || true)" "0" + +check "the rows are unchanged by any of it" "$(q 'SELECT count(*) FROM av_c')" "$live_before" +check "and the content is unchanged" \ + "$(q "SELECT md5(string_agg(id::text||':'||v, ',' ORDER BY id)) FROM av_c")" "$hash_before" + +# ---- the control: heap reports parallel vacuum activity, columnar does not ---- +# If a future release makes table AMs participate, this check flips and should be +# rewritten to assert that our vacuum reports workers too. +vh="$(psql_run 'VACUUM (VERBOSE) av_h;' 2>&1)" +vc="$(psql_run 'VACUUM (VERBOSE) av_c;' 2>&1)" +check "control: VACUUM VERBOSE reports on the heap table" \ + "$(grep -qi 'finished vacuuming' <<<"$vh" && echo yes || echo no)" "yes" +check "columnar VACUUM VERBOSE reports nothing, because the AM path is ours" \ + "$(grep -qi 'finished vacuuming .*av_c' <<<"$vc" && echo "reports" || echo "silent")" "silent" + +pgc_summary diff --git a/test/run_all_versions.sh b/test/run_all_versions.sh index 8725ba6..df988fb 100755 --- a/test/run_all_versions.sh +++ b/test/run_all_versions.sh @@ -208,7 +208,7 @@ SRCDIR="${PGC_RUN_SRCDIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}" SUITES=(harness_selftest docs_style smoke phase2 phase3 phase4 phase5 phase6 audit concurrency unique_conc \ differential recovery replication native_backend_crash fuzz fuzz_parquet fuzz_arrow hardening concurrent_diff parallel sorted_projection \ arrow_export parquet_export read_stream corruption \ - generated_columns temporal arrow_import index_only projections arrow_nested parquet_import parquet_nested arrow_nested_import parquet_nested_import native_writer native_roundtrip native_encoding native_fastdecode native_zonemap write_minmax_fastpath write_fsst_compressed fsst_margin encode_invariants encode_effort native_skip pushdown_report native_agg native_agg_deletes native_agg_addcolumn native_groupagg ungrouped_vector_agg parallel_vector_agg native_bloom bloom_setting bloom_lazy native_vecskip native_index native_fetch_position native_dml alter_column_type native_ios native_projection native_cluster native_compact native_recluster recluster_extent native_vacuum_race native_sort_by sort_status native_reclaim native_ownership drop_cleanup pg_dump_roundtrip native_reclaim_cycles native_reclaim_frag native_reclaim_reconcile native_gap native_format native_truncate native_rewrite native_rewrite_conc rewrite_group_scan native_parquet_schema native_read_parquet native_parquet_fdw native_parquet_pushdown native_parquet_hardening server_file_privilege native_parquet_stack native_parquet_units native_parquet_flba native_parquet_codecs native_parquet_projection native_parquet_multifile native_parquet_streaming native_parquet_partition native_cancel cancel_decode wal_envelope decode_interrupts import_exclusion import_deferred parallel_copy parallel_export_parquet fk_referencing row_triggers native_lazy_slot native_ctas native_fetch_cache native_fetch_interrupt analyze_stats analyze_reltuples native_fetch_projection column_projection isolation) + generated_columns temporal arrow_import index_only projections arrow_nested parquet_import parquet_nested arrow_nested_import parquet_nested_import native_writer native_roundtrip native_encoding native_fastdecode native_zonemap write_minmax_fastpath write_fsst_compressed fsst_margin encode_invariants encode_effort native_skip pushdown_report native_agg native_agg_deletes native_agg_addcolumn native_groupagg ungrouped_vector_agg parallel_vector_agg native_bloom bloom_setting bloom_lazy native_vecskip native_index native_fetch_position native_dml alter_column_type native_ios native_projection native_cluster pg19_vacuum_options native_compact native_recluster recluster_extent native_vacuum_race native_sort_by sort_status native_reclaim native_ownership drop_cleanup pg_dump_roundtrip native_reclaim_cycles native_reclaim_frag native_reclaim_reconcile native_gap native_format native_truncate native_rewrite native_rewrite_conc rewrite_group_scan native_parquet_schema native_read_parquet native_parquet_fdw native_parquet_pushdown native_parquet_hardening server_file_privilege native_parquet_stack native_parquet_units native_parquet_flba native_parquet_codecs native_parquet_projection native_parquet_multifile native_parquet_streaming native_parquet_partition native_cancel cancel_decode wal_envelope decode_interrupts import_exclusion import_deferred parallel_copy parallel_export_parquet fk_referencing row_triggers native_lazy_slot native_ctas native_fetch_cache native_fetch_interrupt analyze_stats analyze_reltuples native_fetch_projection column_projection isolation) # Default matrix: one assert-enabled pg_config per major, 15 through 19. DEFAULT_CONFIGS=(