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
12 changes: 12 additions & 0 deletions test/check_ledger.tsv
Original file line number Diff line number Diff line change
Expand Up @@ -2296,6 +2296,18 @@ partman_columnar partman_columnar premise: with default_table_access_method ever
partman_columnar partman_columnar premise: without default_table_access_method pg_partman makes heap partitions 15;16;17;18;19 never -
partman_columnar partman_columnar retention leaves no pgcolumnar storage row behind 15;16;17;18;19 2026-10-12 the drop hook stops deleting the storage row, so a partition pg_partman drops leaves it behind
partman_columnar partman_columnar the rows went in 15;16;17;18;19 2026-10-12 a sequential scan drops the last row group from its list, so every one-group partition reads empty
prepared_across_sessions prepared_across_sessions ROLLBACK PREPARED of a DELETE leaves every row as it was 15;16;17;18;19 never -
prepared_across_sessions prepared_across_sessions ROLLBACK PREPARED of an UPDATE leaves every row as it was, none duplicated 15;16;17;18;19 never -
prepared_across_sessions prepared_across_sessions a prepared insert is invisible to another session 15;16;17;18;19 never -
prepared_across_sessions prepared_across_sessions a prepared insert survives a crash: the cluster came back and still holds the prepared transaction 15;16;17;18;19 never -
prepared_across_sessions prepared_across_sessions a second insert of a key the prepared transaction holds waits, then fails with 23505 after COMMIT PREPARED 15;16;17;18;19 never -
prepared_across_sessions prepared_across_sessions an UPDATE of a row a prepared transaction updated waits, then fails with 40001 after COMMIT PREPARED 15;16;17;18;19 2026-10-15 the tree before #1421: a same-row UPDATE on a keyed table fails with 23505 rather than 40001
prepared_across_sessions prepared_across_sessions and after COMMIT PREPARED its rows are heap's 15;16;17;18;19 never -
prepared_across_sessions prepared_across_sessions and after COMMIT PREPARED the other session sees all the prepared rows 15;16;17;18;19 never -
prepared_across_sessions prepared_across_sessions premise: prepared transactions are enabled 15;16;17;18;19 never -
prepared_across_sessions prepared_across_sessions premise: the DELETE is prepared 15;16;17;18;19 never -
prepared_across_sessions prepared_across_sessions premise: the UPDATE is prepared 15;16;17;18;19 never -
prepared_across_sessions prepared_across_sessions while a DELETE is prepared, another session still sees every row 15;16;17;18;19 never -
prepared_writes prepared_writes a delete prepared and committed in one session leaves the session answering 15;16;17;18;19 2026-10-13 PREPARE letting go of neither (main before #1323);PREPARE without letting go of the delete-vector buffers
prepared_writes prepared_writes an index fetch before PREPARE, and the same row fetched first after COMMIT PREPARED, in one session 15;16;17;18;19 never -
prepared_writes prepared_writes an insert prepared and committed in one session leaves the session answering 15;16;17;18;19 2026-10-13 PREPARE letting go of neither (main before #1323);PREPARE without letting go of the pending write states
Expand Down
16 changes: 15 additions & 1 deletion test/check_ledger_budget.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2316,4 +2316,18 @@ suites_not_covered 232
# never 2179 + 6 = 2185
# dated 694 + 9 = 703
# 2185 + 703 == 2888
checks_never_observed_red 2185
#
# #1341, a prepared transaction seen from other sessions and across a crash
# (jdatcmd, stacked on #1422): 2185 -> 2196. prepared_across_sessions enters with all
# 12 checks from five-major runs (12/12 on each of 15-19). One is dated on PG17 by
# the tree before #1421, where an UPDATE of a row a prepared transaction updated
# failed with 23505. The other nine arrive never: a discard at PRE_PREPARE did not
# redden them, because each statement's writes are flushed at its executor end.
# Two more premises from the #1428 review, that the DELETE and the UPDATE are each
# prepared before their ROLLBACK PREPARED arms, arrive never.
#
# ledger rows 2888 + 12 = 2900
# never 2185 + 11 = 2196
# dated 703 + 1 = 704
# 2196 + 704 == 2900
checks_never_observed_red 2196
105 changes: 105 additions & 0 deletions test/prepared_across_sessions.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
#!/usr/bin/env bash
#
# A prepared transaction, seen from other sessions and across a crash (#1341).
# prepared_writes.sh covers PREPARE and COMMIT PREPARED in one session. Untested
# until now:
#
# - what another session sees while a transaction is prepared;
# - the unique-key and row locks a prepared transaction still holds: a second
# writer of the same key or row waits, then fails once COMMIT PREPARED runs;
# - ROLLBACK PREPARED of a DELETE and of an UPDATE;
# - a prepared insert surviving a crash and committing after recovery.
#
# The columnar table has a primary key; a heap table loaded with the same rows is the
# answer where one is needed. Waits are observed, not slept on.
#
# Usage: test/prepared_across_sessions.sh [PG_CONFIG]
# Written fresh for pgColumnar.

set -uo pipefail
export PGC_EXTRA_CONF='max_prepared_transactions=4'
. "$(dirname "${BASH_SOURCE[0]}")/lib.sh"
pgc_setup "${1:-/usr/local/pg17/bin/pg_config}"

check "premise: prepared transactions are enabled" "$(q "SHOW max_prepared_transactions")" "4"
psql_run "CREATE TABLE px (k int PRIMARY KEY, v text) USING pgcolumnar;
INSERT INTO px SELECT g, 'r' || g FROM generate_series(1, 1000) g;"
rows() { q "SELECT count(*) || '/' || md5(string_agg(k || ':' || v, ',' ORDER BY k)) FROM px" | grep -E '^[0-9]+/' | tail -1; }
P() { env PATH="$PGC_BINDIR:$PATH" psql -h 127.0.0.1 -p "$PGC_PORT" -U postgres -d "$PGC_DB" -Atq -v VERBOSITY=sqlstate "$@"; }
sqlstate_of() { sed -n 's/^ERROR: *\([0-9A-Z]\{5\}\).*/\1/p' "$1" | tail -1; }
waits_on_lock() { # waits_on_lock APPLICATION_NAME -> yes once that session waits on a lock
local i
for i in $(seq 1 300); do
[ "$(q "SELECT count(*) FROM pg_stat_activity WHERE application_name = '$1' AND wait_event_type = 'Lock'")" = 1 ] && { echo yes; return; }
sleep 0.1
done
echo no
}

# ---- a prepared insert, from another session ------------------------------------------
psql_run "BEGIN; INSERT INTO px SELECT g, 'p' || g FROM generate_series(1001, 1100) g; PREPARE TRANSACTION 'px_ins';" >/dev/null
check "a prepared insert is invisible to another session" "$(q "SELECT count(*) FROM px WHERE k > 1000")" "0"
P -c "SET application_name = 'px_dup'; SET statement_timeout = '60s';" -c "INSERT INTO px VALUES (1050, 'dup');" \
>"$PGC_WORKDIR/px_dup.out" 2>&1 &
dup=$!
waited="$(waits_on_lock px_dup)"
psql_run "COMMIT PREPARED 'px_ins';" >/dev/null
wait "$dup"
check "a second insert of a key the prepared transaction holds waits, then fails with 23505 after COMMIT PREPARED" \
"$waited|$(sqlstate_of "$PGC_WORKDIR/px_dup.out")|$(q "SELECT count(*) || '/' || min(v) FROM px WHERE k = 1050")" "yes|23505|1/p1050"
check "and after COMMIT PREPARED the other session sees all the prepared rows" "$(q "SELECT count(*) FROM px WHERE k > 1000")" "100"

# ---- ROLLBACK PREPARED of a DELETE and of an UPDATE -----------------------------------
before="$(rows)"
psql_run "BEGIN; DELETE FROM px WHERE k % 20 = 0; PREPARE TRANSACTION 'px_del';" >/dev/null
# Without these premises a PREPARE that failed would leave the table unchanged, and
# the checks after it would compare it with itself (OffgridwithJD, review of #1428).
check "premise: the DELETE is prepared" "$(q "SELECT count(*) FROM pg_prepared_xacts WHERE gid = 'px_del'")" "1"
check "while a DELETE is prepared, another session still sees every row" "$(rows)" "$before"
psql_run "ROLLBACK PREPARED 'px_del';" >/dev/null
check "ROLLBACK PREPARED of a DELETE leaves every row as it was" "$(rows)" "$before"
psql_run "BEGIN; UPDATE px SET v = v || '!' WHERE k % 20 = 1; PREPARE TRANSACTION 'px_upd';" >/dev/null
check "premise: the UPDATE is prepared" "$(q "SELECT count(*) FROM pg_prepared_xacts WHERE gid = 'px_upd'")" "1"
psql_run "ROLLBACK PREPARED 'px_upd';" >/dev/null
check "ROLLBACK PREPARED of an UPDATE leaves every row as it was, none duplicated" "$(rows)" "$before"

# ---- a row lock carried across PREPARE --------------------------------------------------
psql_run "BEGIN; UPDATE px SET v = 'prepared' WHERE k = 7; PREPARE TRANSACTION 'px_row';" >/dev/null
P -c "SET application_name = 'px_rowc'; SET statement_timeout = '60s';" -c "UPDATE px SET v = 'other' WHERE k = 7;" \
>"$PGC_WORKDIR/px_rowc.out" 2>&1 &
rc=$!
waited="$(waits_on_lock px_rowc)"
psql_run "COMMIT PREPARED 'px_row';" >/dev/null
wait "$rc"
check "an UPDATE of a row a prepared transaction updated waits, then fails with 40001 after COMMIT PREPARED" \
"$waited|$(sqlstate_of "$PGC_WORKDIR/px_rowc.out")|$(q "SELECT count(*) || '/' || min(v) FROM px WHERE k = 7")" "yes|40001|1/prepared"

# ---- a prepared insert across a crash -------------------------------------------------
crash_cluster() {
local pm i
pm="$(head -1 "$PGC_PGDATA/postmaster.pid" 2>/dev/null || true)"
[ -n "$pm" ] && { kill -9 -"$pm" 2>/dev/null || true; kill -9 "$pm" 2>/dev/null || true; }
for i in $(seq 1 100); do { [ -n "$pm" ] && kill -0 "$pm" 2>/dev/null; } || break; sleep 0.1; done
sleep 1
}
restart_cluster() {
local attempt
for attempt in 1 2 3 4 5 6; do
pgc_pg "pg_ctl -D '$PGC_PGDATA' -l '$PGC_LOGFILE' start -w" >/dev/null 2>&1 && { echo ok; return; }
sleep 2
done
echo "restart failed"
}
psql_run "CREATE TABLE px_heap (k int PRIMARY KEY, v text);
INSERT INTO px_heap SELECT g, md5(g::text) FROM generate_series(5001, 10000) g;
BEGIN; INSERT INTO px SELECT g, md5(g::text) FROM generate_series(5001, 10000) g; PREPARE TRANSACTION 'px_crash';" >/dev/null
crash_cluster
back="$(restart_cluster)"
check "a prepared insert survives a crash: the cluster came back and still holds the prepared transaction" \
"$back|$(q "SELECT count(*) FROM pg_prepared_xacts WHERE gid = 'px_crash'")" "ok|1"
psql_run "COMMIT PREPARED 'px_crash';" >/dev/null
check "and after COMMIT PREPARED its rows are heap's" \
"$(q "SELECT count(*) || '/' || md5(string_agg(k || ':' || v, ',' ORDER BY k)) FROM px WHERE k > 5000")" \
"$(q "SELECT count(*) || '/' || md5(string_agg(k || ':' || v, ',' ORDER BY k)) FROM px_heap")"

pgc_summary
11 changes: 11 additions & 0 deletions test/pytest/TESTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ behaviour, the source of that number is named.
- [113. test_vacuum_lock_privilege.py: maintenance refuses a non-owner before taking its lock](#113-test_vacuum_lock_privilegepy-maintenance-refuses-a-non-owner-before-taking-its-lock)
- [114. test_vm_privilege.py: the visibility-map entry points are owner-only and refuse heap](#114-test_vm_privilegepy-the-visibility-map-entry-points-are-owner-only-and-refuse-heap)
- [115. test_partition_rows.py: rows cross columnar partitions, and keys hold through the parent](#115-test_partition_rowspy-rows-cross-columnar-partitions-and-keys-hold-through-the-parent)
- [116. test_prepared_across_sessions.py: a prepared transaction, seen from other sessions and across a crash](#116-test_prepared_across_sessionspy-a-prepared-transaction-seen-from-other-sessions-and-across-a-crash)

## 1. How to read a test in here

Expand Down Expand Up @@ -6969,3 +6970,13 @@ The pytest twin of `test/partition_rows.sh` (#1341, #1421), every check by the s
| --- | --- |
| `test_rows_cross_columnar_partitions_and_keys_hold_through_the_parent` | the row-moving update against heap, the three key arms, and the three races with each contender seen waiting |
| `test_a_keyed_update_keeps_its_memory_bounded` | an UPDATE of 25,000 keyed rows, in place and moving partition, over a row group with deletes, grows the backend's memory by under 32 MB (#1422 review) |

## 116. test_prepared_across_sessions.py: a prepared transaction, seen from other sessions and across a crash

The pytest twin of `test/prepared_across_sessions.sh` (#1341), every check by the shell's name, on a private cluster because the last arm crashes the postmaster. While a transaction is prepared, another session does not see its insert and still sees the rows its DELETE removed. A second insert of a key it holds waits and gets 23505 after COMMIT PREPARED; an UPDATE of a row it updated waits and gets 40001, which needs #1421's fix (it got 23505 before, the table having a primary key). ROLLBACK PREPARED of a DELETE and of an UPDATE leaves every row as it was. A prepared insert survives an immediate stop, is still listed in `pg_prepared_xacts`, and after COMMIT PREPARED its rows equal a heap table's.

### Every test

| test | what it holds |
| --- | --- |
| `test_a_prepared_transaction_across_sessions_and_a_crash` | visibility while prepared, the two waits and their errors, both ROLLBACK PREPAREDs, and the crash |
4 changes: 3 additions & 1 deletion test/pytest/expected_tests.txt
Original file line number Diff line number Diff line change
Expand Up @@ -677,4 +677,6 @@ guard_tests 435
# over the cluster files on this tree.
# cluster_tests 568 -> 569 for #1422 review's test_a_keyed_update_keeps_its_memory_bounded
# in test_partition_rows.py, by collection over the cluster files on this tree.
cluster_tests 569
# cluster_tests 569 -> 570 for #1341's test_prepared_across_sessions.py, one test, by
# collection over the cluster files on this tree (stacked on #1422).
cluster_tests 570
1 change: 1 addition & 0 deletions test/pytest/test_compare_to_bash.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@
"partition_lifecycle",
"partition_rows",
"partman_columnar",
"prepared_across_sessions",
"prepared_writes",
"projection_drop_column",
"projection_parallel",
Expand Down
Loading
Loading