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
33 changes: 21 additions & 12 deletions test/check_ledger.tsv
Original file line number Diff line number Diff line change
Expand Up @@ -2261,6 +2261,7 @@ maintenance_crash maintenance_crash compact_rewrite changed the row groups befor
maintenance_crash maintenance_crash compact_rewrite changed the row groups in its open transaction 15;16;17;18;19 2026-10-15 compact_rewrite rewrites nothing (rewritten = 0)
maintenance_crash maintenance_crash compact_rewrite committed, and the cluster came back after a crash before any checkpoint 15;16;17;18;19 never -
maintenance_crash maintenance_crash compact_rewrite ran in its open transaction, and the cluster came back after a crash 15;16;17;18;19 never -
maintenance_crash maintenance_crash premise: the by-index reading is an Index Scan through mc_id 15;16;17;18;19 never -
maintenance_crash maintenance_crash recluster changed the row groups before the crash 15;16;17;18;19 never -
maintenance_crash maintenance_crash recluster changed the row groups in its open transaction 15;16;17;18;19 never -
maintenance_crash maintenance_crash recluster committed, and the cluster came back after a crash before any checkpoint 15;16;17;18;19 never -
Expand All @@ -2269,15 +2270,16 @@ maintenance_crash maintenance_crash vacuum changed the row groups before the cra
maintenance_crash maintenance_crash vacuum changed the row groups in its open transaction 15;16;17;18;19 never -
maintenance_crash maintenance_crash vacuum committed, and the cluster came back after a crash before any checkpoint 15;16;17;18;19 never -
maintenance_crash maintenance_crash vacuum ran in its open transaction, and the cluster came back after a crash 15;16;17;18;19 never -
maintenance_rollback maintenance_rollback after a savepoint rolls back two rewrites, the rows written later in the transaction commit beside the old ones 15;16;17;18;19 2026-10-15 PgColumnarAllocateFreeSpace reuses space whatever its freeing xid, so a rolled-back maintenance function's freed bytes are overwritten
maintenance_rollback maintenance_rollback after compact rolls back, the table holds the same rows and row groups, by scan and by index 15;16;17;18;19 never -
maintenance_rollback maintenance_rollback after compact_rewrite rolls back, the table holds the same rows and row groups, by scan and by index 15;16;17;18;19 2026-10-15 PgColumnarAllocateFreeSpace reuses space whatever its freeing xid, so a rolled-back maintenance function's freed bytes are overwritten
maintenance_rollback maintenance_rollback after recluster rolls back, the table holds the same rows and row groups, by scan and by index 15;16;17;18;19 never -
maintenance_rollback maintenance_rollback after vacuum rolls back, the table holds the same rows and row groups, by scan and by index 15;16;17;18;19 never -
maintenance_rollback maintenance_rollback after vacuum_sorted rolls back, the table holds the same rows and row groups, by scan and by index 15;16;17;18;19 never -
maintenance_rollback maintenance_rollback after a savepoint rolls back two rewrites, the rows written later in the transaction commit beside the old ones 15;16;17;18;19 2026-10-16 PgColumnarAllocateFreeSpace reuses space whatever its freeing xid, so a rolled-back maintenance function's freed bytes are overwritten;columnar_tableam.c: pgcolumnar_index_fetch_tuple finds no row
maintenance_rollback maintenance_rollback after compact rolls back, the table holds the same rows and row groups, by scan and by index 15;16;17;18;19 2026-10-16 columnar_tableam.c: pgcolumnar_index_fetch_tuple finds no row
maintenance_rollback maintenance_rollback after compact_rewrite rolls back, the table holds the same rows and row groups, by scan and by index 15;16;17;18;19 2026-10-16 PgColumnarAllocateFreeSpace reuses space whatever its freeing xid, so a rolled-back maintenance function's freed bytes are overwritten;columnar_tableam.c: pgcolumnar_index_fetch_tuple finds no row
maintenance_rollback maintenance_rollback after recluster rolls back, the table holds the same rows and row groups, by scan and by index 15;16;17;18;19 2026-10-16 columnar_tableam.c: pgcolumnar_index_fetch_tuple finds no row
maintenance_rollback maintenance_rollback after vacuum rolls back, the table holds the same rows and row groups, by scan and by index 15;16;17;18;19 2026-10-16 columnar_tableam.c: pgcolumnar_index_fetch_tuple finds no row
maintenance_rollback maintenance_rollback after vacuum_sorted rolls back, the table holds the same rows and row groups, by scan and by index 15;16;17;18;19 2026-10-16 columnar_tableam.c: pgcolumnar_index_fetch_tuple finds no row
maintenance_rollback maintenance_rollback and every function then runs for real and keeps the same rows 15;16;17;18;19 never -
maintenance_rollback maintenance_rollback compact changed the row groups inside its transaction 15;16;17;18;19 never -
maintenance_rollback maintenance_rollback compact_rewrite changed the row groups inside its transaction 15;16;17;18;19 never -
maintenance_rollback maintenance_rollback premise: the by-index reading is an Index Scan through mr_id 15;16;17;18;19 never -
maintenance_rollback maintenance_rollback premise: the table holds the rows it was given 15;16;17;18;19 never -
maintenance_rollback maintenance_rollback recluster changed the row groups inside its transaction 15;16;17;18;19 never -
maintenance_rollback maintenance_rollback vacuum changed the row groups inside its transaction 15;16;17;18;19 never -
Expand Down Expand Up @@ -2426,14 +2428,21 @@ native_decode_gating native_decode_gating the needle removes exactly the non-mat
native_decode_gating native_decode_gating the needle skips exactly the vectors holding no match 15;16;17;18;19 2026-10-16 columnar_reader.c: the per-vector qual evaluation between the decode passes never runs;columnar_reader.c: the per-vector qual evaluator marks every vector skipped
native_decode_gating native_decode_gating the nomatch query returns nothing 15;16;17;18;19 never -
native_decode_gating native_decode_gating the zero-match query removes every row by filter, counted once 15;16;17;18;19 never -
native_delete_visibility_paths native_delete_visibility_paths a DELETE whose qual deletes another row of the table succeeds and leaves heap's rows (#1394) 15;16;17;18;19 2026-10-14 PgColumnarUpsertDeleteVector without the command advance (main before #1394's fix)
native_delete_visibility_paths native_delete_visibility_paths a deleted row is invisible via the index 15;16;17;18;19 never -
native_delete_visibility_paths native_delete_visibility_paths a DELETE whose qual deletes another row of the table succeeds and leaves heap's rows (#1394) 15;16;17;18;19 2026-10-16 PgColumnarUpsertDeleteVector without the command advance (main before #1394's fix);columnar_reader.c: the row-at-a-time scan reads a deleted row as live
native_delete_visibility_paths native_delete_visibility_paths a deleted row is invisible via the index 15;16;17;18;19 2026-10-16 columnar_reader.c: pgcolumnar_fetch_row ignores the flushed delete vector
native_delete_visibility_paths native_delete_visibility_paths a live row is visible via the index 15;16;17;18;19 never -
native_delete_visibility_paths native_delete_visibility_paths a row deleted in-transaction is invisible in the same transaction 15;16;17;18;19 never -
native_delete_visibility_paths native_delete_visibility_paths a row deleted in-transaction is invisible in the same transaction 15;16;17;18;19 2026-10-16 columnar_reader.c: pgcolumnar_fetch_row ignores the flushed delete vector
native_delete_visibility_paths native_delete_visibility_paths aggregate over the whole table sees the live set 15;16;17;18;19 never -
native_delete_visibility_paths native_delete_visibility_paths index / bitmap scan sees the live set 15;16;17;18;19 never -
native_delete_visibility_paths native_delete_visibility_paths index-only scan sees the live set 15;16;17;18;19 never -
native_delete_visibility_paths native_delete_visibility_paths sequential scan sees the live set (row-emitting path) 15;16;17;18;19 never -
native_delete_visibility_paths native_delete_visibility_paths index / bitmap scan sees the live set 15;16;17;18;19 2026-10-16 columnar_reader.c: pgcolumnar_fetch_row ignores the flushed delete vector
native_delete_visibility_paths native_delete_visibility_paths index-only scan sees the live set 15;16;17;18;19 2026-10-16 columnar_reader.c: pgcolumnar_fetch_row ignores the flushed delete vector
native_delete_visibility_paths native_delete_visibility_paths premise: the deletes span more than one row group 15;16;17;18;19 never -
native_delete_visibility_paths native_delete_visibility_paths premise: the index arm really plans an Index Scan 15;16;17;18;19 never -
native_delete_visibility_paths native_delete_visibility_paths premise: the index-only arm really plans an Index Only Scan 15;16;17;18;19 never -
native_delete_visibility_paths native_delete_visibility_paths premise: the sequential arm really plans a Seq Scan 15;16;17;18;19 never -
native_delete_visibility_paths native_delete_visibility_paths premise: the table holds all 5000 rows 15;16;17;18;19 never -
native_delete_visibility_paths native_delete_visibility_paths premise: the whole-table aggregate stays on the columnar path 15;16;17;18;19 never -
native_delete_visibility_paths native_delete_visibility_paths sequential scan sees the live set (row-emitting path) 15;16;17;18;19 2026-10-16 columnar_reader.c: the row-at-a-time scan reads a deleted row as live
native_delete_visibility_paths native_delete_visibility_paths the rolled-back delete left the row in place 15;16;17;18;19 never -
native_dict_underfill native_dict_underfill backend survives the under-filled DICT chunk 15;16;17;18;19 2026-10-16 columnar_encoding.c: the DICT fill refusal replaced by a backend crash
native_dict_underfill native_dict_underfill premise: the inflation rewrote the DICT chunk's descriptor 15;16;17;18;19 never -
native_dict_underfill native_dict_underfill the text column is DICT-encoded (precondition) 15;16;17;18;19 never -
Expand Down
24 changes: 24 additions & 0 deletions test/check_ledger_budget.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3443,4 +3443,28 @@ suites_not_covered 193
# never 3010 + 7 = 3017
# dated 1300 + 6 = 1306
# 3017 + 1306 == 4323
#
# #1339, native_delete_visibility_paths' shell arms take the paths they name
# (OffgridwithJD): 3017 -> 3019. Seven checks are new, all never: the six path and
# fixture premises the twin already had, and the rolled-back row. Five arms that
# entered never are now dated: the index / bitmap, index-only, deleted-row-via-index
# and in-transaction arms by pgcolumnar_fetch_row ignoring the delete vector, and
# the sequential arm by the row scan reading a deleted row as live. The #1394 arm
# gains the second in its history.
#
# ledger rows 4323 + 7 = 4330
# never 3017 + 7 - 5 = 3019
# dated 1306 + 5 = 1311
# 3019 + 1311 == 4330
#
# #1339, maintenance_rollback's and maintenance_crash's by-index readings read the
# index (OffgridwithJD): 3019 -> 3017. Two premises are new, one per suite, both
# never. With pgcolumnar_index_fetch_tuple finding no row, maintenance_rollback's
# six "by index" arms are red; four of them entered never and are now dated, and
# the other two gain the mutation in their history. No row is removed.
#
# ledger rows 4330 + 2 = 4332
# never 3019 + 2 - 4 = 3017
# dated 1311 + 4 = 1315
# 3017 + 1315 == 4332
checks_never_observed_red 3017
16 changes: 13 additions & 3 deletions test/maintenance_crash.sh
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,16 @@ rows() {
}
RG_SQL="SELECT count(*) || '/' || coalesce(sum(row_count), 0) FROM pgcolumnar.row_group
WHERE storage_id = pgcolumnar.get_storage_id('mc')"
BY_INDEX_SETS="SET enable_seqscan = off; SET enable_bitmapscan = off; SET pgcolumnar.enable_custom_scan = off;"
BY_INDEX_SQL="SELECT count(*) || '/' || md5(string_agg(id || ':' || v || ':' || w, ',' ORDER BY id))
FROM mc WHERE id BETWEEN 1 AND 10000"
by_index() {
q "SET enable_seqscan = off; SET enable_bitmapscan = off;
SELECT count(*) || '/' || md5(string_agg(id || ':' || v || ':' || w, ',' ORDER BY id))
FROM mc WHERE id BETWEEN 1 AND 10000" | grep -E '^[0-9]+/' | tail -1
# pgcolumnar.enable_custom_scan = off as well, and a premise that this plan is an
# Index Scan: enable_seqscan alone leaves the columnar custom scan on, and this
# whole-table range planned as Custom Scan (PgColumnarScan). Measured (#1339):
# with the index fetch reading the neighbouring row, every "by index" arm stayed
# green, because none of them read the index.
q "$BY_INDEX_SETS $BY_INDEX_SQL" | grep -E '^[0-9]+/' | tail -1
}
state() { # the table, as "<rows>|<row groups>|<rows by index>"
echo "$(rows)|$(q "$RG_SQL" | grep -E '^[0-9]+/' | tail -1)|$(by_index)"
Expand All @@ -75,6 +81,10 @@ fresh_table() {
CHECKPOINT;" >/dev/null
}

fresh_table
check "premise: the by-index reading is an Index Scan through mc_id" \
"$(pgc_uses_row_fetch "$BY_INDEX_SETS" "$BY_INDEX_SQL")" yes

FUNCS=("pgcolumnar.compact_rewrite('mc', 0.0)" "pgcolumnar.recluster('mc', 'v')" "pgcolumnar.compact('mc')" "pgcolumnar.vacuum('mc')")

# ---- uncommitted at the crash ---------------------------------------------------------
Expand Down
14 changes: 11 additions & 3 deletions test/maintenance_rollback.sh
Original file line number Diff line number Diff line change
Expand Up @@ -40,14 +40,22 @@ rows() { # the rows, by content
}
RG_SQL="SELECT count(*) || '/' || coalesce(sum(row_count), 0) FROM pgcolumnar.row_group
WHERE storage_id = pgcolumnar.get_storage_id('mr')"
BY_INDEX_SETS="SET enable_seqscan = off; SET enable_bitmapscan = off; SET pgcolumnar.enable_custom_scan = off;"
BY_INDEX_SQL="SELECT count(*) || '/' || md5(string_agg(id || ':' || v || ':' || w, ',' ORDER BY id))
FROM mr WHERE id BETWEEN 1 AND 10000"
by_index() { # the rows an index scan returns, by content
q "SET enable_seqscan = off; SET enable_bitmapscan = off;
SELECT count(*) || '/' || md5(string_agg(id || ':' || v || ':' || w, ',' ORDER BY id))
FROM mr WHERE id BETWEEN 1 AND 10000" | grep -E '^[0-9]+/' | tail -1
# pgcolumnar.enable_custom_scan = off as well, and a premise that this plan is an
# Index Scan: enable_seqscan alone leaves the columnar custom scan on, and this
# whole-table range planned as Custom Scan (PgColumnarScan). Measured (#1339):
# with the index fetch reading the neighbouring row, every "by index" arm stayed
# green, because none of them read the index.
q "$BY_INDEX_SETS $BY_INDEX_SQL" | grep -E '^[0-9]+/' | tail -1
}
ROWS0="$(rows)"
RG0="$(q "$RG_SQL")"
check "premise: the table holds the rows it was given" "${ROWS0%%/*}|$RG0" "5400|10/10000"
check "premise: the by-index reading is an Index Scan through mr_id" \
"$(pgc_uses_row_fetch "$BY_INDEX_SETS" "$BY_INDEX_SQL")" yes

# rolled_back FUNCTION -> "<groups inside the transaction>|<rows after>|<groups after>|<by index after>"
rolled_back() {
Expand Down
59 changes: 48 additions & 11 deletions test/native_delete_visibility_paths.sh
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,14 @@ q "CREATE EXTENSION IF NOT EXISTS pgcolumnar;" >/dev/null
q "SET pgcolumnar.stripe_row_limit=1000;
CREATE TABLE t (id int, v int) USING pgcolumnar;
INSERT INTO t SELECT g, g FROM generate_series(1,5000) g;
CREATE UNIQUE INDEX t_id ON t (id);
DELETE FROM t WHERE id % 7 = 0;" >/dev/null
CREATE UNIQUE INDEX t_id ON t (id);" >/dev/null
check "premise: the table holds all 5000 rows" "$(q 'SELECT count(*) FROM t;')" "5000"
q "DELETE FROM t WHERE id % 7 = 0;" >/dev/null
# Each group builds its own mask, so one group would test the fold of one bitmap.
check "premise: the deletes span more than one row group" \
"$(q "SELECT (count(*) >= 2)::int FROM pgcolumnar.storage s
JOIN pgcolumnar.row_group rg USING (storage_id)
WHERE s.relation_oid = 't'::regclass;")" "1"

# Live count = 5000 - floor(5000/7) = 5000 - 714 = 4286. Five row groups.
LIVE=4286
Expand All @@ -36,27 +42,58 @@ LIVE=4286
# multi-statement send.
last() { q "$1" | tail -1; }

# A qual on the non-indexed column forces a real row-emitting sequential scan
# (which applies the delete mask per row), not the unqualified count(*) metadata
# fast-path (which subtracts delete counts and never reaches the bit test).
# EACH ARM FORCES THE PATH IT IS NAMED FOR, AND ASSERTS THE NODE FIRST (#1339).
# The arms set enable_seqscan/enable_indexonlyscan and trusted the names, but none
# of the enable_* scan GUCs governs Custom Scan (PgColumnarScan): measured, the
# index / bitmap and index-only arms planned as the columnar custom scan, the same
# path as the sequential arm, so three of four named paths were one path counted
# three times. pgcolumnar.enable_custom_scan=off is what moves the plan; the
# pytest twin has forced each path this way since it was written.
NO_COLUMNAR="SET pgcolumnar.enable_custom_scan = off;"
path_plan() { # path_plan SETS QUERY NODE -> yes, or the plan's node lines
local plan
plan="$(q "$1 EXPLAIN (COSTS OFF) $2")"
case "$plan" in
*"$3"*) echo yes ;;
*) echo "no: $(grep -E 'Scan|Aggregate' <<<"$plan" | sed 's/^ *//' | tr '\n' '/')" ;;
esac
}

SEQ="$NO_COLUMNAR SET enable_indexscan=off; SET enable_bitmapscan=off; SET enable_indexonlyscan=off;"
check "premise: the sequential arm really plans a Seq Scan" \
"$(path_plan "$SEQ" "SELECT count(*) FROM t WHERE v >= 0" "Seq Scan")" yes
check "sequential scan sees the live set (row-emitting path)" \
"$(last 'SET enable_indexscan=off; SET enable_bitmapscan=off; SET enable_indexonlyscan=off; SELECT count(*) FROM t WHERE v >= 0;')" "$LIVE"
"$(last "$SEQ SELECT count(*) FROM t WHERE v >= 0;")" "$LIVE"

IDX="$NO_COLUMNAR SET enable_seqscan=off; SET enable_indexonlyscan=off;"
check "premise: the index arm really plans an Index Scan" \
"$(path_plan "$IDX" "SELECT count(*) FROM t WHERE id >= 0" "Index Scan")" yes
check "index / bitmap scan sees the live set" \
"$(last 'SET enable_seqscan=off; SET enable_indexonlyscan=off; SELECT count(*) FROM t WHERE id >= 0;')" "$LIVE"
"$(last "$IDX SELECT count(*) FROM t WHERE id >= 0;")" "$LIVE"

IOS="$NO_COLUMNAR SET enable_seqscan=off; SET enable_indexonlyscan=on;"
check "premise: the index-only arm really plans an Index Only Scan" \
"$(path_plan "$IOS" "SELECT count(id) FROM t WHERE id >= 0" "Index Only Scan")" yes
check "index-only scan sees the live set" \
"$(last 'SET enable_seqscan=off; SET enable_indexonlyscan=on; SELECT count(id) FROM t WHERE id >= 0;')" "$LIVE"
"$(last "$IOS SELECT count(id) FROM t WHERE id >= 0;")" "$LIVE"

# The columnar path, left to the planner: a different consumer of the same fold.
check "premise: the whole-table aggregate stays on the columnar path" \
"$(path_plan "" "SELECT count(*) FROM t" "Custom Scan")" yes
check "aggregate over the whole table sees the live set" \
"$(q 'SELECT count(*) FROM t;')" "$LIVE"

# A specific deleted row is invisible, and its live neighbour visible, via the
# index fetch (which reaches the same bit test through a different path).
# index (which reaches the same bit test through a different path).
check "a deleted row is invisible via the index" \
"$(last 'SET enable_seqscan=off; SELECT count(*) FROM t WHERE id = 700;')" "0"
"$(last "$NO_COLUMNAR SET enable_seqscan=off; SELECT count(*) FROM t WHERE id = 700;")" "0"
check "a live row is visible via the index" \
"$(last 'SET enable_seqscan=off; SELECT count(*) FROM t WHERE id = 701;')" "1"
"$(last "$NO_COLUMNAR SET enable_seqscan=off; SELECT count(*) FROM t WHERE id = 701;")" "1"
# The buffered (same-transaction) path: delete more inside a txn, then read it back.
check "a row deleted in-transaction is invisible in the same transaction" \
"$(q 'BEGIN; DELETE FROM t WHERE id = 8; SELECT count(*) FROM t WHERE id = 8; ROLLBACK;' | grep -E '^[0-9]+$' | head -1)" "0"
# and the rollback really rolled back, so the arm above is not a delete that persisted
check "the rolled-back delete left the row in place" "$(q 'SELECT count(*) FROM t WHERE id = 8;')" "1"

# A DELETE whose qual deletes another row of the same table (#1394). The inner DELETE's
# scan start writes the outer statement's buffered marks, and its statement end writes
Expand Down
Loading
Loading