Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
14 commits
Select commit Hold shift + click to select a range
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
5 changes: 5 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,11 @@ jobs:
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
# The docs_style twin compares each dated CHANGELOG section with its release tag
# (#1341). The default checkout fetches no tags, so without this its four changelog
# arms would decline here on every run. Depth 1, as in the suites job.
- name: fetch the release tags, for the changelog section comparisons
run: git fetch --depth=1 origin '+refs/tags/v1.0-alpha*:refs/tags/v1.0-alpha*'
- name: install pytest, pinned from requirements-test.txt, WITHOUT the driver
run: |
set -euo pipefail
Expand Down
127 changes: 70 additions & 57 deletions test/audit.sh
Original file line number Diff line number Diff line change
Expand Up @@ -62,10 +62,10 @@ set -euo pipefail
# not turn an abort into a pass; it stops an abort from being mistaken for a
# short run that went fine.
AUDIT_REACHED_END=0
trap 'rc=$?; if [ "$AUDIT_REACHED_END" = 0 ]; then
echo; echo "AUDIT TEST ABORTED (exit $rc) before reaching its verdict.";
echo "The checks after the failing command did not run.";
fi' EXIT
# THE NOTICE LIVES IN cleanup() BELOW, NOT IN A trap OF ITS OWN. It was a trap here,
# and `trap cleanup EXIT` further down replaced it, so it never printed: with
# set_options failing at entry the suite stopped after the partitioned arm, 13 checks
# unrecorded, and the log ended on the server's error with no notice at all.

# lib.sh for the check vocabulary (#965). It sources portlib.sh itself
# (lib.sh:110), so this is a superset of what was here, and its top level is
Expand Down Expand Up @@ -121,8 +121,14 @@ fi
run_pg() { "${RUNPG[@]}" env PATH="$BINDIR:$PATH" bash -lc "$1"; }

cleanup() {
local rc=$?
if [ "$AUDIT_REACHED_END" = 0 ]; then
echo; echo "AUDIT TEST ABORTED (exit $rc) before reaching its verdict."
echo "The checks after the failing command did not run."
fi
run_pg "pg_ctl -D '$PGDATA' stop -m immediate -w" >/dev/null 2>&1 || true
rm -rf "$WORKDIR"
return $rc
}
trap cleanup EXIT

Expand All @@ -137,46 +143,36 @@ run_pg "createdb -p $PORT audit"
PSQL="psql -p $PORT -d audit -qAtX -v ON_ERROR_STOP=1"
q() { run_pg "$PSQL -c \"$1\""; }

fail=0
# RECORDS RATHER THAN ONLY PRINTING (#965). This suite emitted human PASS lines
# and no RESULT records, so every mechanism built on the record vocabulary -- the
# ledger, the census, checks_never_observed_red, the red-observation record,
# duplicate-name detection -- was blind to all of them. The ledger did not merely
# return nothing on this log, it REFUSED it: "no RESULT records, so there is
# nothing to reconcile".
#
# `pgc_record` takes the DISPLAY whole, so the human output below is byte-for-byte
# what it was. NOT lib.sh's own `check`: that composes its own display and would
# drop the `: $got` suffix, which is the measured value rather than a label.
# LIB.SH'S check, NOT A LOCAL ONE (#1341). This suite used to define its own
# check() and call pgc_record from it, to keep the measured value on every PASS
# line. Nothing else here needs a local recorder, and a suite that calls
# pgc_record directly cannot have a pytest twin (test_compare_to_bash's seed
# arm), so it records through lib.sh like the rest of the corpus. A FAIL line still
# carries both sides; a PASS line carries the name.

# expect_error NAME SQL MESSAGE -- PASS when SQL is refused WITH MESSAGE.
#
# `fail` is still set, so this suite's exit logic is untouched. Its verdict line
# stays for the same reason: under `set -euo pipefail` a failing command aborts the
# suite, and the verdict is what distinguishes finished from stopped.
# ANY error used to pass. Measured on PG17 with set_options failing at entry for a
# reason that is none of its guards (a division by zero): all five refusal arms below
# passed, while the two that already read the reason went red. MESSAGE is the guard's
# own text, so a refusal must come from the guard the arm names.
#
# The `checks run:` line is READ BY NOTHING YET. The matrix gates reconciliation on
# the ACCOUNTING line via `pgc_log_shows_accounting`, and this suite emits none
# because it emits no accounting line. The line is still correct and wanted --
# it is the total the records reconcile against -- so the remaining step is a gate
# flip rather than new work (@jdatcmd, #969 review).
check() {
local name="$1" got="$2" want="$3"
if [ "$got" = "$want" ]; then
pgc_record PASS "$name" "PASS $name: $got"
else
pgc_record FAIL "$name" "FAIL $name: got [$got] want [$want]"
fail=1
fi
}

# run SQL that is expected to raise an error; PASS when it does.
# The name stays `expect_error`: selftest 270 recognises an out-of-range set_options
# literal as deliberate only inside this helper.
expect_error() {
local name="$1" sql="$2"
if run_pg "$PSQL -c \"$sql\"" >/dev/null 2>&1; then
pgc_record FAIL "$name" "FAIL $name: statement unexpectedly succeeded"
fail=1
local name="$1" sql="$2" want="$3" out
local got
if out="$(run_pg "$PSQL -c \"$sql\"" 2>&1)"; then
got="statement unexpectedly succeeded"
elif [ -z "$want" ]; then
got="no refusal message to require (harness)"
else
pgc_record PASS "$name" "PASS $name (rejected)"
case "$out" in
*"$want"*) got="rejected" ;;
*) got="refused for another reason: $(printf '%s\n' "$out" | head -1)" ;;
esac
fi
check "$name" "$got" "rejected"
}

q "CREATE EXTENSION pgcolumnar;" >/dev/null
Expand Down Expand Up @@ -262,11 +258,14 @@ q "DROP TABLE col;" >/dev/null
# ---------------------------------------------------------------------------
q "CREATE TABLE opt (a int) USING pgcolumnar;" >/dev/null
expect_error "reject chunk_group_row_limit 0" \
"SELECT pgcolumnar.set_options('opt'::regclass, chunk_group_row_limit => 0);"
"SELECT pgcolumnar.set_options('opt'::regclass, chunk_group_row_limit => 0);" \
"chunk_group_row_limit must be at least 100"
expect_error "reject stripe_row_limit 5" \
"SELECT pgcolumnar.set_options('opt'::regclass, stripe_row_limit => 5);"
"SELECT pgcolumnar.set_options('opt'::regclass, stripe_row_limit => 5);" \
"stripe_row_limit must be at least 1000"
expect_error "reject compression_level 99" \
"SELECT pgcolumnar.set_options('opt'::regclass, compression_level => 99);"
"SELECT pgcolumnar.set_options('opt'::regclass, compression_level => 99);" \
"compression_level must be between 1 and 22"

# A relation that is not columnar must be refused rather than recorded. The drop
# hook that clears pgcolumnar.options fires only for columnar relations, so a row
Expand All @@ -276,9 +275,10 @@ expect_error "reject compression_level 99" \
# as the bare oid, while the identical sequence on a columnar table cleaned up.
q "CREATE TABLE opt_heap (a int) USING heap;" >/dev/null
expect_error "reject set_options on a heap table" \
"SELECT pgcolumnar.set_options('opt_heap'::regclass, chunk_group_row_limit => 1000);"
# expect_error passes on ANY error, including a typo in this file's own SQL, so
# assert the reason too. Captured and then matched: piping an erroring psql into
"SELECT pgcolumnar.set_options('opt_heap'::regclass, chunk_group_row_limit => 1000);" \
"is not a columnar table"
# expect_error now requires the guard's message too; this check reads the full
# reason the same way, and stays as its own record. Captured and then matched: piping an erroring psql into
# grep reports on the pipeline rather than on the message.
# `|| true` because this file runs under `set -e` and the call is EXPECTED to
# fail: without it the assignment takes the non-zero status and aborts the suite
Expand Down Expand Up @@ -332,13 +332,16 @@ if [ "$audit_srv" -ge 170000 ]; then
"$(q "SELECT c.relkind::text || '/' || a.amname FROM pg_class c JOIN pg_am a ON a.oid = c.relam WHERE c.relname = 'opt_part';")" \
"p/pgcolumnar"
expect_error "reject set_options on a partitioned parent" \
"SELECT pgcolumnar.set_options('opt_part'::regclass, chunk_group_row_limit => 1000);"
"SELECT pgcolumnar.set_options('opt_part'::regclass, chunk_group_row_limit => 1000);" \
"is not a columnar table"
check "nothing was recorded for the partitioned parent" \
"$(q "SELECT count(*) FROM pgcolumnar.options WHERE regclass = 'opt_part'::regclass;")" "0"
# And the leaf, which is an ordinary table, must still be accepted -- the guard
# has to refuse the parent without refusing the thing that actually stores rows.
q "CREATE TABLE opt_part_1 PARTITION OF opt_part FOR VALUES FROM (0) TO (100);" >/dev/null
q "SELECT pgcolumnar.set_options('opt_part_1'::regclass, chunk_group_row_limit => 1000);" >/dev/null
# `|| true`: a set_options that fails here is what the next check reports, by name.
# Under set -e it used to end the suite instead, with every later check unrecorded.
q "SELECT pgcolumnar.set_options('opt_part_1'::regclass, chunk_group_row_limit => 1000);" >/dev/null || true
check "the partition itself is still accepted" \
"$(q "SELECT count(*) FROM pgcolumnar.options WHERE regclass = 'opt_part_1'::regclass;")" "1"
q "DROP TABLE opt_part;" >/dev/null
Expand All @@ -355,8 +358,9 @@ else
fi

# valid values are accepted, and delete/update work (no divide-by-zero)
# `|| true` for the same reason: "is recorded" below names a failed call.
q "SELECT pgcolumnar.set_options('opt'::regclass,
chunk_group_row_limit => 1000, stripe_row_limit => 2000, compression_level => 9);" >/dev/null
chunk_group_row_limit => 1000, stripe_row_limit => 2000, compression_level => 9);" >/dev/null || true
q "INSERT INTO opt SELECT g FROM generate_series(1,10) g;" >/dev/null
q "DELETE FROM opt WHERE a=5;" >/dev/null
check "valid options delete works" "$(q "SELECT count(*) FROM opt;")" "9"
Expand All @@ -378,18 +382,27 @@ q "DROP TABLE opt;" >/dev/null
q "SET pgcolumnar.stripe_row_limit=10000;
CREATE TABLE idxleak (a int, b int) USING pgcolumnar WITH (parallel_workers=2);" >/dev/null
q "INSERT INTO idxleak SELECT g, g%100 FROM generate_series(1,50000) g;" >/dev/null
run_pg "$PSQL -c \"SET max_parallel_maintenance_workers=2; SET min_parallel_table_scan_size=0;
CREATE INDEX idxleak_a_idx ON idxleak(a);\"" >/dev/null 2>&1 || true
# THE BUILD'S OUTCOME IS RECORDED, NOT DISCARDED. It was `>/dev/null 2>&1 || true`.
# Measured on PG17 with a parallel build participant raising an error: CREATE INDEX
# failed, no index existed, and "no relation leak" passed (nothing was built, so
# nothing leaked) and "rows indexed once" passed (50000, counted by a Seq Scan).
# Only the plan check went red, and it was labelled a premise.
idx_out="$(run_pg "$PSQL -c \"SET max_parallel_maintenance_workers=2; SET min_parallel_table_scan_size=0;
CREATE INDEX idxleak_a_idx ON idxleak(a);\"" 2>&1)" && idx_rc=0 || idx_rc=$?
check "the parallel CREATE INDEX completed" \
"$([ "$idx_rc" = 0 ] && echo yes || echo "no: $(printf '%s\n' "$idx_out" | head -1)")" "yes"

# the server logfile must carry no resource-leak warning
LEAKS="$(run_pg "grep -c 'resource was not closed' '$LOGFILE'" || true)"
check "create index no relation leak" "$LEAKS" "0"

# and every row is indexed exactly once (no duplicate TIDs from a per-worker rescan)
check "premise: the indexed-once count reads idxleak_a_idx" \
"$(reads_index idxleak_a_idx "SELECT count(*) FROM idxleak WHERE a>0")" "yes"
# NOT A PREMISE: a failed parallel build reddens it (#1339). And the count is read
# only when that plan reads the index, so a Seq Scan cannot answer for it.
idx_plan="$(reads_index idxleak_a_idx "SELECT count(*) FROM idxleak WHERE a>0")"
check "the indexed-once count reads idxleak_a_idx" "$idx_plan" "yes"
check "create index rows indexed once" \
"$(q "$IDXONLY SELECT count(*) FROM idxleak WHERE a>0;" | tail -1)" "50000"
"$([ "$idx_plan" = yes ] && q "$IDXONLY SELECT count(*) FROM idxleak WHERE a>0;" | tail -1 || echo "not read through idxleak_a_idx")" "50000"
q "DROP TABLE idxleak;" >/dev/null

# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -454,13 +467,13 @@ q "DROP TABLE ss_dup;" >/dev/null

echo
AUDIT_REACHED_END=1
if [ "$fail" = "0" ]; then
echo "checks run: $PGC_CHECKS"
# The verdict is lib.sh's, from the counters every check updates. pgc_summary adds
# the accounting line the matrix reconciles on, which this suite never printed.
if [ "${PGC_FAIL:-0}" = "0" ]; then
echo "AUDIT TEST PASSED"
else
echo "checks run: $PGC_CHECKS"
echo "AUDIT TEST FAILED"
echo "---- server log tail ----"
run_pg "tail -30 '$LOGFILE'" || true
fi
exit $fail
pgc_summary
Loading
Loading