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 CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -400,6 +400,18 @@ true until the next version shipped.
Reported by @OffgridwithJD.

### Fixed
- **In the upgrade window, a refusal for a missing catalog table names `ALTER
EXTENSION UPDATE`.** After the new library is installed and the server restarted,
but before `ALTER EXTENSION pgcolumnar UPDATE`, `DROP TABLE`, `TRUNCATE` and
`pgcolumnar.vacuum()` on a columnar table fail, because the 1.0-alpha5 library
needs `pgcolumnar.retired_group` and a 1.0-alpha4 catalog lacks it. They change
nothing. The error used to name only the missing table, and it now carries a hint:
`The pgcolumnar catalog in this database may be older than the installed library.
Run ALTER EXTENSION pgcolumnar UPDATE in this database.` Reads and writes work
throughout, and after the update every one of these succeeds on the same rows
(#1341 item 66). `test/native_upgrade_converge.sh` and
`test_native_upgrade_converge.py`.


- **A join whose runtime-filter build is empty no longer reads the fact table.** When
a large dimension filtered to no row was joined to a columnar table, the hash join
Expand Down
12 changes: 12 additions & 0 deletions docs/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,18 @@ The fix is step 3. Run `ALTER EXTENSION pgcolumnar UPDATE;` in that database and
the error goes away. **Your data is not affected.** The tables are intact and no
conversion happens. Only the catalog entry is stale.

Between releases that add a catalog table, the new library can also need a table
the old catalog does not have. Upgrading from `1.0-alpha4` to `1.0-alpha5`,
`DROP TABLE`, `TRUNCATE` and `pgcolumnar.vacuum()` on a columnar table fail until
step 3, and change nothing; reads and writes keep working:

```
ERROR: columnar metadata table "pgcolumnar.retired_group" does not exist
HINT: The pgcolumnar catalog in this database may be older than the installed library. Run ALTER EXTENSION pgcolumnar UPDATE in this database.
```

The fix is the same: step 3.

Do not use `DROP EXTENSION` instead. A plain `DROP EXTENSION pgcolumnar` fails
while columnar tables exist, because they depend on the access method. The
form that succeeds is `DROP EXTENSION pgcolumnar CASCADE`, and it drops every
Expand Down
13 changes: 12 additions & 1 deletion src/columnar_metadata.c
Original file line number Diff line number Diff line change
Expand Up @@ -239,11 +239,22 @@ open_columnar_table(const char *name, LOCKMODE lockmode)
nspOid = pgcolumnar_schema_oid();
relOid = get_relname_relid(name, nspOid);

/*
* NAME THE REMEDY (#1341 item 66). The usual way here is the upgrade window:
* the new library is installed and the server restarted (docs/installation.md
* steps 1 and 2), but this database's catalog is still the previous release's
* until ALTER EXTENSION UPDATE (step 3). A catalog table this library added,
* such as retired_group in 1.0-alpha5, is then missing, and DROP TABLE,
* TRUNCATE and pgcolumnar.vacuum() stop here. The error alone named only the
* table, which says neither why nor what to do.
*/
if (!OidIsValid(relOid))
ereport(ERROR,
(errcode(ERRCODE_UNDEFINED_TABLE),
errmsg("columnar metadata table \"%s.%s\" does not exist",
COLUMNAR_SCHEMA_NAME, name)));
COLUMNAR_SCHEMA_NAME, name),
errhint("The pgcolumnar catalog in this database may be older than the installed library. "
"Run ALTER EXTENSION pgcolumnar UPDATE in this database.")));

rel = table_open(relOid, lockmode);

Expand Down
136 changes: 136 additions & 0 deletions test/check_ledger.tsv

Large diffs are not rendered by default.

92 changes: 90 additions & 2 deletions test/check_ledger_budget.txt
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
#
# Adding a check to a suite that is already covered does not move it, which is
# what makes it safe to bound.
suites_not_covered 219
suites_not_covered 213
#
# checks_never_observed_red -- A CENSUS. NOT a ceiling, and it must not become
# one.
Expand Down Expand Up @@ -2943,4 +2943,92 @@ suites_not_covered 219
# never 2676 - 1 = 2675
# dated 943 + 1 = 944
# 2675 + 944 == 3619
checks_never_observed_red 2675
#
# #1341, fk_referencing seeded, with its twin (OffgridwithJD): 2675 -> 2686, and
# suites_not_covered 219 -> 218. The suite had no rows. It enters with 28 rows
# from five-major runs: 27 checks on PG17-19 and 26 on 15-16, where the
# partitioned-control branch asserts core's refusal instead of a conversion. 17 are
# dated by four mutations of the foreign-key guards: the referenced-side refusal
# removed, the guard keyed on the referencing table, the conversion refusal
# removed, and the conversion refused whatever the target access method (the new
# referenced-table-to-row-store arm). Eleven arrive never: the over-rejection
# controls that no single guard reaches (another constraint, a heap-to-heap key,
# an unreferenced conversion, an unrelated ALTER, a conversion back to heap, the
# referencing side, the partitioned control on either side of 17) and the primary
# key's own arms.
#
# ledger rows 3619 + 28 = 3647
# never 2675 + 11 = 2686
# dated 944 + 17 = 961
# 2686 + 961 == 3647
#
# #1341, truncate_cleanup seeded, with its twin (OffgridwithJD): 2686 -> 2695, and
# suites_not_covered 218 -> 217. The suite had no rows. It enters with all 25 checks
# from five-major runs (25/25 on each of 15-19). 16 are dated by four mutations of
# the truncate paths: a rewrite that no longer deletes the retired storage's rows
# (7), nor drops the stale write state (1), an in-place truncate that skips the
# projections' storages (8), and a storage-tree delete that skips them (1). Nine
# arrive never: the premises that each fixture holds rows or metadata, the
# surviving projection declaration, the empty table after TRUNCATE, and the
# rewrite that keeps every row.
#
# ledger rows 3647 + 25 = 3672
# never 2686 + 9 = 2695
# dated 961 + 16 = 977
# 2695 + 977 == 3672
#
# #1341, native_dml seeded, with its twin (OffgridwithJD): 2695 -> 2702, and
# suites_not_covered 217 -> 216. The suite had no rows. It enters with all 15 checks
# from five-major runs (15/15 on each of 15-19). 8 are dated by three mutations: a
# row group holding deletes still answering min and max from its zone maps (the
# min/max arm, which could not see it before this change), count(*) from metadata
# not subtracting deleted rows (3), and the index fetch ignoring the delete vector
# (5). Seven arrive never: the two premises, the rows and sum after the delete, the
# deleted rows being gone, the live row's index lookup, and the primary key arm.
#
# ledger rows 3672 + 15 = 3687
# never 2695 + 7 = 2702
# dated 977 + 8 = 985
# 2702 + 985 == 3687
#
# #1341, native_dict_underfill seeded, with its twin (OffgridwithJD): 2702 -> 2704,
# and suites_not_covered 216 -> 215. The suite had no rows. It enters with all 4
# checks from five-major runs (4/4 on each of 15-19). 2 are dated by two mutations
# of the DICT fill check: removed, so the garbage tail is read back with no error,
# and replaced by a backend crash, which the rejection and the log-counted survival
# arm both catch. The two premises arrive never.
#
# ledger rows 3687 + 4 = 3691
# never 2702 + 2 = 2704
# dated 985 + 2 = 987
# 2704 + 987 == 3691
#
# #1341, native_decode_gating seeded, with its twin (OffgridwithJD): 2704 -> 2722, and
# suites_not_covered 215 -> 214. The suite had no rows. It enters with all 29 checks
# from five-major runs (29/29 on each of 15-19). 11 are dated by four mutations of
# #452 phase 2: the per-vector evaluation never run (4), a deleted row counted as
# filtered (1, the new arm), every vector marked skipped (7), and the producer
# counting a gated group's rejections twice (2, one of them new). Eighteen arrive
# never: the thirteen premises, the observable's presence, the full scan across
# every group, the nomatch query's empty answer, and two exact counters that no
# single mutation here moves.
#
# ledger rows 3691 + 29 = 3720
# never 2704 + 18 = 2722
# dated 987 + 11 = 998
# 2722 + 998 == 3720
#
# #1341, native_decode_gate_width seeded, with its twin (OffgridwithJD): 2722 -> 2754,
# and suites_not_covered 214 -> 213. The suite had no rows. It enters with all 35
# checks from five-major runs (35/35 on each of 15-19). 3 are dated by three
# mutations of the #595 width gate: gating whatever the width (the two "not gated"
# arms), > for >= (the boundary arm), and a payload count one too high (the
# raised-threshold arm). Thirty-two arrive never: the twenty-one premises, and the arms
# the width policy does not move -- the gated counts, the gate-off and controls, the
# correctness arms -- which the sibling native_decode_gating dates.
#
# ledger rows 3720 + 35 = 3755
# never 2722 + 32 = 2754
# dated 998 + 3 = 1001
# 2754 + 1001 == 3755
checks_never_observed_red 2754
10 changes: 10 additions & 0 deletions test/fk_referencing.sh
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,16 @@ check "converting back to heap is unaffected" \
"$(case "$back" in *ERROR*) echo "no ($back)" ;; *) echo yes ;; esac)" \
"yes"

# AND ON THE REFERENCED TABLE ITSELF (#1341). The control above converts a table
# with no foreign key, so it cannot see a rule that refuses every SET ACCESS METHOD
# on a referenced table whatever the target: with the guard's columnar-target test
# removed, this file stayed 26/26. Only a columnar target needs the row lock the
# constraint cannot get.
refheap="$(psql_run "ALTER TABLE fk_conv_parent SET ACCESS METHOD heap;" 2>&1 || true)"
check "an FK-referenced table may still be set to a row store" \
"$(case "$refheap" in *ERROR*) echo "no ($refheap)" ;; *) echo yes ;; esac)" \
"yes"

# --- 7. a partitioned parent is refused too (#201) -----------------------------

# A partitioned table has no storage, so setting its access method breaks nothing
Expand Down
11 changes: 9 additions & 2 deletions test/native_decode_gate_width.sh
Original file line number Diff line number Diff line change
Expand Up @@ -106,10 +106,17 @@ check "premise: the width-gate GUC exists and defaults to the shipped $DEFAULT_G
# unknown parameter errors; q swallows the error and returns empty, so an empty
# result is the proof the parameter does not exist. The paired positive premise
# above (min_payload_cols == 20) rules out an empty result from a dead connection.
# By SQLSTATE rather than by an empty result (#1341): q returns empty for a dead
# connection as well, and an empty expectation is the shape the pytest layer refuses.
show_state() { # show_state PARAM -> the SQLSTATE SHOW raised, or "shown"
env PATH="$PGC_BINDIR:$PATH" psql -h 127.0.0.1 -p "$PGC_PORT" -U postgres -d "$PGC_DB" \
-qAtX -v VERBOSITY=verbose -c "SHOW $1;" >/dev/null 2>"$PGC_WORKDIR/show.err" && { echo shown; return; }
sed -nE 's/^ERROR: ([0-9A-Z]{5}): .*/\1/p' "$PGC_WORKDIR/show.err" | head -1
}
check "premise: the rejected probe GUC qual_skipvec_probe_vecs was NOT shipped" \
"$(q "SHOW pgcolumnar.qual_skipvec_probe_vecs;")" ""
"$(show_state pgcolumnar.qual_skipvec_probe_vecs)" "42704"
check "premise: the rejected probe GUC qual_skipvec_min_skip_pct was NOT shipped" \
"$(q "SHOW pgcolumnar.qual_skipvec_min_skip_pct;")" ""
"$(show_state pgcolumnar.qual_skipvec_min_skip_pct)" "42704"
check "premise: one row group, so every count below is one group's" \
"$(q "SELECT count(*) FROM pgcolumnar.row_group WHERE storage_id = pgcolumnar.get_storage_id('nw');")" \
"1"
Expand Down
32 changes: 30 additions & 2 deletions test/native_decode_gating.sh
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,20 @@ check "the zero-match query removes every row by filter, counted once" \
check "and skips every vector" \
"$(counter_in "$PLAN_NOMATCH" 'Columnar Vectors Skipped')" "$VECTORS"

# A SURVIVING VECTOR WITH REJECTS IN IT (#1341). Every row of the needle's
# surviving vector matches, so the producer, re-testing that vector to find its
# rows, finds nothing to reject: a producer that counted those rejections a second
# time read exactly right above. Measured, with the producer's non-counting filter
# disabled, this file stayed 26/26. Half of vector 0 passes this qual (p1 % 2 = 0 is
# an expression, so it is no skip predicate either), so its 512 rejections are
# counted twice by that mistake.
HALFQ="SELECT * FROM n WHERE tag LIKE '%needle%' AND p1 % 2 = 0"
PLAN_HALF="$(explain_of "$HALFQ")"
check "premise: the half-needle survives in one vector, half of its rows" \
"$(q "SELECT count(*) || '|' || count(DISTINCT (p1 - 1) / 1024) FROM n WHERE tag LIKE '%needle%' AND p1 % 2 = 0;")" "512|1"
check "rejections inside a surviving vector are counted once" \
"$(counter_in "$PLAN_HALF" 'Rows Removed by Filter')" "$(( ROWS - 512 ))"

# ---- correctness -----------------------------------------------------------
#
# The mask must never drop a row. Without these, an implementation that marks
Expand Down Expand Up @@ -205,13 +219,18 @@ MGEN="SELECT 'x' || CASE WHEN g % 4096 = 7 THEN 'needle' ELSE 'miss' END || g::t
psql_run "INSERT INTO hm $MGEN;"
psql_run "INSERT INTO m $MGEN;"
psql_run "DELETE FROM hm WHERE p1 % 17 = 0;"
psql_run "DELETE FROM m WHERE p1 % 17 = 0;"
# The fixture's DELETE must not depend on the gating under test: its WHERE is an
# unprunable qual too, so it is run with the width gate out of reach (#1341).
psql_run "SET pgcolumnar.qual_skipvec_min_payload_cols = 100000; DELETE FROM m WHERE p1 % 17 = 0;"

check "premise: the columnar table spans more than one row group" \
"$(q "SELECT count(*) > 1 FROM pgcolumnar.row_group WHERE storage_id = pgcolumnar.get_storage_id('m');")" \
"t"
# count(*) rather than a count WHERE p1 % 17 = 0: that WHERE is an unprunable qual
# too, so it ran through the very gating under test, and a gating mutant broke this
# premise rather than the arms below (#1341).
check "premise: rows really were deleted (so the mask is non-NULL)" \
"$(q "SELECT count(*) FROM m WHERE p1 % 17 = 0;")" "0"
"$(q "SELECT count(*) FROM m;")" "$(( MROWS - MROWS / 17 ))"

check "deletes + multiple groups + unprunable qual: columnar matches the heap" \
"$(pgc_set_hash "SELECT * FROM m WHERE tag LIKE '%needle%'")" \
Expand All @@ -220,4 +239,13 @@ check "and the full scan across every group's mask matches too" \
"$(pgc_set_hash "SELECT * FROM m")" \
"$(pgc_set_hash "SELECT * FROM hm")"

# AND THE COUNTER SKIPS DELETED ROWS (#1341). The evaluator must not count a deleted
# row as filtered: it is invisible. Only the single-group table above had its
# counters checked, and it has no deletes, so with the evaluator's deleted-row test
# removed this file stayed 26/26.
PLAN_M="$(explain_of "SELECT * FROM m WHERE tag LIKE '%needle%'")"
check "rows removed by filter across deleted rows count only live rows" \
"$(counter_in "$PLAN_M" 'Rows Removed by Filter')" \
"$(q "SELECT count(*) FROM hm WHERE tag NOT LIKE '%needle%';")"

pgc_summary
30 changes: 19 additions & 11 deletions test/native_dict_underfill.sh
Original file line number Diff line number Diff line change
Expand Up @@ -27,23 +27,31 @@ echo "-- v-column vector0 encoding type byte (DICT=6): $ENC"
check "the text column is DICT-encoded (precondition)" "$ENC" "6"

# read old rawLen (uint32 LE at descriptor offset 11), inflate by 64, write back
q "UPDATE pgcolumnar.column_chunk c SET encoding_descriptor =
crashes() {
# grep -c prints 0 itself when nothing matches, and then exits 1
pgc_pg "grep -c 'terminated by signal' '$PGC_LOGFILE'" 2>/dev/null || true
}
crashes0="$(crashes)"
q "WITH u AS (UPDATE pgcolumnar.column_chunk c SET encoding_descriptor =
set_byte(set_byte(set_byte(set_byte(encoding_descriptor,
11, ( (get_byte(encoding_descriptor,11) + (get_byte(encoding_descriptor,12)<<8) + (get_byte(encoding_descriptor,13)<<16) + (get_byte(encoding_descriptor,14)<<24)) + 64 ) & 255),
12, (((get_byte(encoding_descriptor,11) + (get_byte(encoding_descriptor,12)<<8) + (get_byte(encoding_descriptor,13)<<16) + (get_byte(encoding_descriptor,14)<<24)) + 64) >> 8) & 255),
13, (((get_byte(encoding_descriptor,11) + (get_byte(encoding_descriptor,12)<<8) + (get_byte(encoding_descriptor,13)<<16) + (get_byte(encoding_descriptor,14)<<24)) + 64) >>16) & 255),
14, (((get_byte(encoding_descriptor,11) + (get_byte(encoding_descriptor,12)<<8) + (get_byte(encoding_descriptor,13)<<16) + (get_byte(encoding_descriptor,14)<<24)) + 64) >>24) & 255)
WHERE c.storage_id=$SID AND c.column_index=1" >/dev/null
WHERE c.storage_id=$SID AND c.column_index=1 RETURNING 1) SELECT count(*) FROM u" > "$PGC_WORKDIR/uf.n"
check "premise: the inflation rewrote the DICT chunk's descriptor" "$(cat "$PGC_WORKDIR/uf.n")" "1"

env PATH="$PGC_BINDIR:$PATH" psql -h 127.0.0.1 -p "$PGC_PORT" -U postgres -d "$PGC_DB" \
-c "SELECT v FROM dt" >/dev/null 2>"$PGC_WORKDIR/uf.err"
-v VERBOSITY=verbose -c "SELECT v FROM dt" >/dev/null 2>"$PGC_WORKDIR/uf.err"
echo "-- select stderr:"; sed 's/^/ /' "$PGC_WORKDIR/uf.err" | head -3
rejected=no
grep -qiE 'does not match|corrupt|invalid|out of' "$PGC_WORKDIR/uf.err" && rejected=yes
crash=no
grep -qiE 'terminated by signal|segmentation|server closed' "$PGC_WORKDIR/uf.err" && crash=yes
grep -qiE 'terminated by signal|segmentation' "$PGC_LOGFILE" 2>/dev/null && crash=yes
echo "-- rejected=$rejected crash=$crash"
check "under-filled DICT chunk is rejected with a clean error" "$rejected" "yes"
check "backend survives the under-filled DICT chunk" "$(q 'SELECT 1')" "1"
# A CLEAN ERROR IS XX001, AND A CRASH IS READ FROM THE SERVER LOG (#1341). The
# rejection used to match 'does not match|corrupt|invalid|out of' in any message,
# which "out of memory" satisfies too; and survival was a fresh SELECT 1, which a
# crashed backend passes once crash recovery has finished. The crash flag below was
# computed and never checked.
rejected="$(sed -nE 's/^ERROR: ([0-9A-Z]{5}): .*/\1/p' "$PGC_WORKDIR/uf.err" | head -1 | grep . || echo "no SQLSTATE ($(head -1 "$PGC_WORKDIR/uf.err"))")"
echo "-- rejected=$rejected crashes=$(( $(crashes) - crashes0 ))"
check "under-filled DICT chunk is rejected with a clean error" "$rejected" "XX001"
check "backend survives the under-filled DICT chunk" \
"$(q 'SELECT 1')|$(( $(crashes) - crashes0 )) backend crashes" "1|0 backend crashes"
pgc_summary
22 changes: 17 additions & 5 deletions test/native_dml.sh
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,15 @@ GEN="SELECT g, (g * 10)::bigint, 'row-' || g FROM generate_series(1, 5000) g"
psql_run "INSERT INTO h $GEN;"
psql_run "INSERT INTO n $GEN;"

# DELETE across several row groups.
psql_run "DELETE FROM h WHERE id % 7 = 0;"
psql_run "DELETE FROM n WHERE id % 7 = 0;"
# DELETE across several row groups, and the table's smallest and largest ids with
# them. Without those two, min and max are the same whether or not the deleted rows
# count, so the min/max arm below could not see a zone-map answer that ignored the
# deletes (#1341): measured, with groups that hold deletes left on the zone-map path,
# this file stayed 13/13.
psql_run "DELETE FROM h WHERE id % 7 = 0 OR id IN (1, 5000);"
psql_run "DELETE FROM n WHERE id % 7 = 0 OR id IN (1, 5000);"
check "premise: the deletes moved the smallest and largest id and k" \
"$(q 'SELECT min(id) || '"'"'|'"'"' || max(id) || '"'"'|'"'"' || min(k) || '"'"'|'"'"' || max(k) FROM h;')" "2|4999|20|49990"

check "count after delete" "$(q 'SELECT count(*) FROM n;')" "$(q 'SELECT count(*) FROM h;')"
check "rows after delete match heap" \
Expand All @@ -32,14 +38,20 @@ check "rows after delete match heap" \
# Aggregates after delete: the zone-map path is bypassed (deletes present), so this
# exercises the delete-applying scan fold.
check "sum(k) after delete" "$(q 'SELECT sum(k) FROM n;')" "$(q 'SELECT sum(k) FROM h;')"
# k as well as id: measured, a zone-map answer that ignored the deletes still gave
# min/max(id) right and min/max(k) wrong (10|50000 for 20|49990).
check "min/max after delete" \
"$(q 'SELECT min(id), max(id) FROM n;')" \
"$(q 'SELECT min(id), max(id) FROM h;')"
"$(q 'SELECT min(id), max(id), min(k), max(k) FROM n;')" \
"$(q 'SELECT min(id), max(id), min(k), max(k) FROM h;')"
check "deleted rows gone" "$(q 'SELECT count(*) FROM n WHERE id % 7 = 0;')" "0"

# Index lookup of a deleted row returns nothing; a live row still resolves.
idx() { env PATH="$PGC_BINDIR:$PATH" psql -h 127.0.0.1 -p "$PGC_PORT" -U postgres -d "$PGC_DB" -q -At -c "SET enable_seqscan=off" -c "$1" 2>/dev/null; }
# id 4444 is live (4444 % 7 = 6); id 4445 was deleted (4445 % 7 = 0).
# The lookups must take the primary-key index, or these arms test the scan instead
# of the index fetch's delete check.
check "premise: the lookups take the primary-key index" \
"$(idx 'EXPLAIN (COSTS off) SELECT count(*) FROM n WHERE id = 4445;' | grep -c 'Index.* using n_pkey')" "1"
check "index finds live row" "$(idx 'SELECT v FROM n WHERE id = 4444;')" "$(q 'SELECT v FROM h WHERE id = 4444;')"
check "index skips deleted row" "$(idx 'SELECT count(*) FROM n WHERE id = 4445;')" "0"

Expand Down
Loading
Loading