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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -416,6 +416,12 @@ true until the next version shipped.
over every row, so a caller limited by a policy could read values from rows it
cannot see. It now raises `row-level security is in force`, as the other functions
that read storage directly do. Found by OffgridwithJD in review.
- **An ungrouped aggregate on a columnar table applies a WHERE that references no
column.** `SELECT count(*), min(v) FROM t WHERE (SELECT false)`, or a WHERE on
`now()` or `current_user`, returned every row's aggregates instead of none. The
vectorized aggregate answered from the zone maps and never applied the one-time
filter. It now leaves such a query to the ordinary plan. Found by OffgridwithJD in
review (#1435). `test/native_agg.sh` and `test_native_agg.py`.

- **A concurrent `UPDATE` of the same row now fails with 40001 on a table with a
unique index, as documented, not 23505 (#1421).** Two sessions updating one row of
Expand Down
15 changes: 15 additions & 0 deletions src/columnar_vector.c
Original file line number Diff line number Diff line change
Expand Up @@ -1562,6 +1562,21 @@ PgColumnarCreateUpperPaths(PlannerInfo *root, UpperRelationKind stage,
* zone-map answerable. A filter, or a sum/avg over int8/float/numeric, needs
* the scan-fold path instead (#289).
*/
/*
* A pseudoconstant (gating) qual -- a WHERE that references no column, such as
* (SELECT false) or now() < '2000-01-01' -- is a one-time filter, and
* extract_actual_clauses(..., false) leaves it out of quals. Refused HERE,
* before needsScan is decided: with no other qual the node answers from the
* zone maps and never reaches the refusal in the scan-fold branch below, so a
* false gate returned every row's aggregates (#1435).
*/
{
ListCell *gc;

foreach(gc, factRel->baserestrictinfo)
if (lfirst_node(RestrictInfo, gc)->pseudoconstant)
return;
}
if (pgcolumnar_quals_could_leak(factRel->baserestrictinfo))
return;
quals = extract_actual_clauses(factRel->baserestrictinfo, false);
Expand Down
62 changes: 62 additions & 0 deletions test/check_ledger.tsv
Original file line number Diff line number Diff line change
Expand Up @@ -2186,6 +2186,9 @@ maintenance_rollback maintenance_rollback premise: the table holds the rows it w
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 -
maintenance_rollback maintenance_rollback vacuum_sorted changed the row groups inside its transaction 15;16;17;18;19 never -
native_agg native_agg a WHERE (SELECT false) with no column in it gives heap's answer 15;16;17;18;19 2026-10-15 the ungrouped vectorized aggregate takes a query with a gating qual and answers from the zone maps (the tree before #1435)
native_agg native_agg a WHERE on current_user with no column in it gives heap's answer 15;16;17;18;19 2026-10-15 the ungrouped vectorized aggregate takes a query with a gating qual and answers from the zone maps (the tree before #1435)
native_agg native_agg a WHERE on now() with no column in it gives heap's answer 15;16;17;18;19 2026-10-15 the ungrouped vectorized aggregate takes a query with a gating qual and answers from the zone maps (the tree before #1435)
native_agg native_agg an aggregate that needs zone maps reads them 15;16;17;18;19 never -
native_agg native_agg avg(int4,nulls) 15;16;17;18;19 never -
native_agg native_agg control: decoding ts reads the table's blocks 15;16;17;18;19 never -
Expand Down Expand Up @@ -2215,6 +2218,8 @@ native_agg native_agg min(ts) reads under a tenth of the blocks a decode of ts r
native_agg native_agg multi-agg 15;16;17;18;19 never -
native_agg native_agg multi-agg beats a parallel plan 15;16;17;18;19 never -
native_agg native_agg multi-agg plan has no Gather 15;16;17;18;19 never -
native_agg native_agg premise: count, min and max with no WHERE are the vectorized aggregate 15;16;17;18;19 never -
native_agg native_agg premise: heap's answer to a false gate is no rows 15;16;17;18;19 never -
native_agg native_agg premise: the timestamptz table holds every row inserted 15;16;17;18;19 never -
native_agg native_agg sum(bigint) fallback parity 15;16;17;18;19 never -
native_agg native_agg sum(int4) 15;16;17;18;19 never -
Expand Down Expand Up @@ -3325,6 +3330,63 @@ ttl_expire ttl_expire the retention is recorded where the other options live 15;
ttl_expire ttl_expire with the extent, expire retires the same groups 15;16;17;18;19 2026-10-13 the early exit compares the extent's maximum to the cutoff instead of its minimum
ttl_expire ttl_expire without the extent the same expire probes every group's zone map 15;16;17;18;19 never -
ttl_expire ttl_expire without the extent, expire retires the aged groups 15;16;17;18;19 2026-10-14 expire returning 0 whatever it retired
type_matrix type_matrix a range on bt finds heap's rows 15;16;17;18;19 never -
type_matrix type_matrix a range on bt holds rows on heap, so its comparison reads something 15;16;17;18;19 2026-10-15 the range bounds cast with pg_typeof, which drops the typmod, so bt's become bit(1) and its range finds no rows
type_matrix type_matrix a range on bt lets the zone maps skip chunk groups 15;16;17;18;19 2026-10-15 the scan never skips a row group by zone map (pgcolumnar_native_group_can_match ignored)
type_matrix type_matrix a range on cd finds heap's rows 15;16;17;18;19 never -
type_matrix type_matrix a range on cd holds rows on heap, so its comparison reads something 15;16;17;18;19 never -
type_matrix type_matrix a range on cd skips no chunk groups: a cidr key is treated as cross-type against inet and dropped (#1433) 15;16;17;18;19 never -
type_matrix type_matrix a range on ch finds heap's rows 15;16;17;18;19 never -
type_matrix type_matrix a range on ch holds rows on heap, so its comparison reads something 15;16;17;18;19 never -
type_matrix type_matrix a range on ch skips no chunk groups: every row group holds every value, since the column cycles through 26 letters 15;16;17;18;19 never -
type_matrix type_matrix a range on d finds heap's rows 15;16;17;18;19 never -
type_matrix type_matrix a range on d holds rows on heap, so its comparison reads something 15;16;17;18;19 never -
type_matrix type_matrix a range on d lets the zone maps skip chunk groups 15;16;17;18;19 2026-10-15 the scan never skips a row group by zone map (pgcolumnar_native_group_can_match ignored)
type_matrix type_matrix a range on dom finds heap's rows 15;16;17;18;19 never -
type_matrix type_matrix a range on dom holds rows on heap, so its comparison reads something 15;16;17;18;19 never -
type_matrix type_matrix a range on dom lets the zone maps skip chunk groups 15;16;17;18;19 2026-10-15 the scan never skips a row group by zone map (pgcolumnar_native_group_can_match ignored)
type_matrix type_matrix a range on e finds heap's rows 15;16;17;18;19 never -
type_matrix type_matrix a range on e holds rows on heap, so its comparison reads something 15;16;17;18;19 never -
type_matrix type_matrix a range on e skips no chunk groups: every row group holds every value, since the column cycles through the 4 labels 15;16;17;18;19 never -
type_matrix type_matrix a range on ip finds heap's rows 15;16;17;18;19 never -
type_matrix type_matrix a range on ip holds rows on heap, so its comparison reads something 15;16;17;18;19 never -
type_matrix type_matrix a range on ip lets the zone maps skip chunk groups 15;16;17;18;19 2026-10-15 the scan never skips a row group by zone map (pgcolumnar_native_group_can_match ignored)
type_matrix type_matrix a range on mac finds heap's rows 15;16;17;18;19 never -
type_matrix type_matrix a range on mac holds rows on heap, so its comparison reads something 15;16;17;18;19 never -
type_matrix type_matrix a range on mac lets the zone maps skip chunk groups 15;16;17;18;19 2026-10-15 the scan never skips a row group by zone map (pgcolumnar_native_group_can_match ignored)
type_matrix type_matrix a range on mac8 finds heap's rows 15;16;17;18;19 never -
type_matrix type_matrix a range on mac8 holds rows on heap, so its comparison reads something 15;16;17;18;19 never -
type_matrix type_matrix a range on mac8 lets the zone maps skip chunk groups 15;16;17;18;19 2026-10-15 the scan never skips a row group by zone map (pgcolumnar_native_group_can_match ignored)
type_matrix type_matrix a range on mo finds heap's rows 15;16;17;18;19 never -
type_matrix type_matrix a range on mo holds rows on heap, so its comparison reads something 15;16;17;18;19 never -
type_matrix type_matrix a range on mo lets the zone maps skip chunk groups 15;16;17;18;19 2026-10-15 the scan never skips a row group by zone map (pgcolumnar_native_group_can_match ignored)
type_matrix type_matrix a range on n finds heap's rows 15;16;17;18;19 never -
type_matrix type_matrix a range on n holds rows on heap, so its comparison reads something 15;16;17;18;19 never -
type_matrix type_matrix a range on n skips no chunk groups: every row group holds NaN and both infinities, so each zone map spans every value 15;16;17;18;19 never -
type_matrix type_matrix a range on nm finds heap's rows 15;16;17;18;19 2026-10-15 a by-reference fixed-width value over 8 bytes is stored as its first 8 bytes and zero padding (PgColumnarEncodeValueByLen)
type_matrix type_matrix a range on nm holds rows on heap, so its comparison reads something 15;16;17;18;19 never -
type_matrix type_matrix a range on nm lets the zone maps skip chunk groups 15;16;17;18;19 2026-10-15 a by-reference fixed-width value over 8 bytes is stored as its first 8 bytes and zero padding (PgColumnarEncodeValueByLen);the scan never skips a row group by zone map (pgcolumnar_native_group_can_match ignored)
type_matrix type_matrix a range on tm finds heap's rows 15;16;17;18;19 never -
type_matrix type_matrix a range on tm holds rows on heap, so its comparison reads something 15;16;17;18;19 never -
type_matrix type_matrix a range on tm lets the zone maps skip chunk groups 15;16;17;18;19 2026-10-15 the scan never skips a row group by zone map (pgcolumnar_native_group_can_match ignored)
type_matrix type_matrix a range on tsi finds heap's rows 15;16;17;18;19 never -
type_matrix type_matrix a range on tsi holds rows on heap, so its comparison reads something 15;16;17;18;19 never -
type_matrix type_matrix a range on tsi lets the zone maps skip chunk groups 15;16;17;18;19 2026-10-15 the scan never skips a row group by zone map (pgcolumnar_native_group_can_match ignored)
type_matrix type_matrix a range on tz finds heap's rows 15;16;17;18;19 2026-10-15 a by-reference fixed-width value over 8 bytes is stored as its first 8 bytes and zero padding (PgColumnarEncodeValueByLen)
type_matrix type_matrix a range on tz holds rows on heap, so its comparison reads something 15;16;17;18;19 never -
type_matrix type_matrix a range on tz lets the zone maps skip chunk groups 15;16;17;18;19 2026-10-15 the scan never skips a row group by zone map (pgcolumnar_native_group_can_match ignored)
type_matrix type_matrix a range on vb finds heap's rows 15;16;17;18;19 never -
type_matrix type_matrix a range on vb holds rows on heap, so its comparison reads something 15;16;17;18;19 never -
type_matrix type_matrix a range on vb lets the zone maps skip chunk groups 15;16;17;18;19 2026-10-15 the scan never skips a row group by zone map (pgcolumnar_native_group_can_match ignored)
type_matrix type_matrix a tsvector match finds heap's rows 15;16;17;18;19 never -
type_matrix type_matrix each column's count and NULL count is heap's 15;16;17;18;19 2026-10-15 a by-reference fixed-width value over 8 bytes is stored as its first 8 bytes and zero padding (PgColumnarEncodeValueByLen)
type_matrix type_matrix equality on json, as text, finds heap's rows 15;16;17;18;19 never -
type_matrix type_matrix every row, as text, is heap's 15;16;17;18;19 2026-10-15 a by-reference fixed-width value over 8 bytes is stored as its first 8 bytes and zero padding (PgColumnarEncodeValueByLen)
type_matrix type_matrix point equality (~=) finds heap's rows 15;16;17;18;19 2026-10-15 a by-reference fixed-width value over 8 bytes is stored as its first 8 bytes and zero padding (PgColumnarEncodeValueByLen)
type_matrix type_matrix premise: both tables hold 12000 rows, the columnar one in at least twelve row groups 15;16;17;18;19 never -
type_matrix type_matrix premise: the server supports xml (built with libxml) 15;16;17;18;19 never -
type_matrix type_matrix the aggregates over numeric NaN and Infinity and timestamp and date infinity are heap's 15;16;17;18;19 never -
type_matrix type_matrix the composite's fields and the enum's order read back as heap's 15;16;17;18;19 never -
vacuum_lock_privilege vacuum_lock_privilege add_projection refuses a non-owner before taking its lock (#749) 15;16;17;18;19 2026-10-14 every maintenance entry point checks ownership after taking its lock
vacuum_lock_privilege vacuum_lock_privilege cluster refuses a non-owner before taking the exclusive lock 15;16;17;18;19 2026-10-14 every maintenance entry point checks ownership after taking its lock
vacuum_lock_privilege vacuum_lock_privilege compact does the same, which is the behaviour drop_projection now matches 15;16;17;18;19 never -
Expand Down
33 changes: 32 additions & 1 deletion test/check_ledger_budget.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2760,4 +2760,35 @@ suites_not_covered 224
# never 2544 + 13 = 2557
# dated 836 + 11 = 847
# 2557 + 847 == 3404
checks_never_observed_red 2557
#
# #1341, types the differential matrix never stored answer as heap (jdatcmd):
# 2557 -> 2596. type_matrix enters with all 57 checks from five-major runs (57/57 on
# each of 15-19). 17 are dated on PG17 by two mutants (OffgridwithJD, reviews of
# #1426):
# - a by-reference fixed-width value over 8 bytes stored as its first 8 bytes and
# zero padding: every row, the counts, the tz range (its zone offset varies, so
# bytes 8-11 matter), the nm range and its pruning, and point equality;
# - the scan never skipping a row group by zone map: the 12 range arms that must
# skip chunk groups, varbit's included now that its values increase with the row.
# A third, the range bounds cast with pg_typeof (which drops the typmod), dates bt's
# heap-not-empty arm: its range then finds no rows. Each of the 16 range arms has
# one, so no comparison can pass on two empty answers (review of #1426).
# The rest arrive never, among them the four range arms pinned at 0 skipped groups,
# with the reason in each name (cidr's names #1433), and the libxml premise.
#
# ledger rows 3404 + 57 = 3461
# never 2557 + 39 = 2596
# dated 847 + 18 = 865
# 2596 + 865 == 3461
#
# #1435, the ungrouped vectorized aggregate ignored a WHERE that references no column
# (jdatcmd): 2596 -> 2598. native_agg gains five checks from five-major runs (40/40 on
# each of 15-19). The three gating arms are dated on PG17 by the tree before the fix,
# where each false gate returned every row's aggregates (10000/1/10000 against
# heap's 0/-/-); the two premises arrive never.
#
# ledger rows 3461 + 5 = 3466
# never 2596 + 2 = 2598
# dated 865 + 3 = 868
# 2598 + 868 == 3466
checks_never_observed_red 2598
14 changes: 14 additions & 0 deletions test/native_agg.sh
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,20 @@ check "count(*) uses the metadata agg node" \
check "sum/min/max uses the metadata agg node" \
"$(has_aggnode "$(plan 'SELECT sum(id), min(id), max(id) FROM n')")" "yes"

# A WHERE that references no column is a gating qual: planned as a one-time
# filter, not part of the per-row quals. The node answered from the zone maps alone
# and ignored it, so a false gate returned every row's aggregates (#1435).
GATED="SELECT count(*) || '/' || coalesce(min(id)::text, '-') || '/' || coalesce(max(id)::text, '-') FROM"
check "premise: count, min and max with no WHERE are the vectorized aggregate" \
"$(has_aggnode "$(plan 'SELECT count(*), min(id), max(id) FROM n')")" "yes"
check "a WHERE (SELECT false) with no column in it gives heap's answer" \
"$(q "$GATED n WHERE (SELECT false)")" "$(q "$GATED h WHERE (SELECT false)")"
check "a WHERE on now() with no column in it gives heap's answer" \
"$(q "$GATED n WHERE now() < '2000-01-01'")" "$(q "$GATED h WHERE now() < '2000-01-01'")"
check "a WHERE on current_user with no column in it gives heap's answer" \
"$(q "$GATED n WHERE current_user = 'pgc_nobody'")" "$(q "$GATED h WHERE current_user = 'pgc_nobody'")"
check "premise: heap's answer to a false gate is no rows" "$(q "$GATED h WHERE (SELECT false)")" "0/-/-"

# Supported aggregates answered from zone maps must equal the heap oracle.
check "count(*)" "$(q 'SELECT count(*) FROM n;')" "$(q 'SELECT count(*) FROM h;')"
check "count(col,nulls)" "$(q 'SELECT count(s) FROM n;')" "$(q 'SELECT count(s) FROM h;')"
Expand Down
12 changes: 12 additions & 0 deletions test/pytest/TESTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@ behaviour, the source of that number is named.
- [128. test_objstore_userinfo.py: userinfo in a URL is refused on every entry point](#128-test_objstore_userinfopy-userinfo-in-a-url-is-refused-on-every-entry-point)
- [129. test_savepoint_flush.py: a savepoint rolled back after a flush](#129-test_savepoint_flushpy-a-savepoint-rolled-back-after-a-flush)
- [130. test_rls_query_paths.py: row-level security holds on every read path](#130-test_rls_query_pathspy-row-level-security-holds-on-every-read-path)
- [131. test_type_matrix.py: types the differential matrix never stored answer as heap](#131-test_type_matrixpy-types-the-differential-matrix-never-stored-answer-as-heap)

## 1. How to read a test in here

Expand Down Expand Up @@ -6486,6 +6487,7 @@ Not ported, and declared INCOMPLETE in `test_compare_to_bash.py`: the `int4`/`te
| test | what it holds |
| --- | --- |
| `test_min_max_over_timestamptz_read_no_data` | the table holds every row; `max(ts)` plans the metadata node; a decode reads at least 10 blocks; `max` and `min` each read under a tenth of that; parity with heap before and after deleting rows at both ends |
| `test_a_where_with_no_column_in_it_is_applied` | a WHERE that references no column, such as `(SELECT false)`, `now() < ...` or `current_user = ...`, gives heap's answer, with a premise that the same aggregate with no WHERE is the vectorized node (#1435) |

## 86. test_format_downgrade.py: an alpha4 binary refuses what this build wrote

Expand Down Expand Up @@ -7175,3 +7177,13 @@ The pytest twin of `test/rls_query_paths.sh` (#1341), every check by the shell's
| test | what it holds |
| --- | --- |
| `test_row_level_security_holds_on_every_read_path` | the read paths against heap (the custom scan, both vectorized aggregates, parallel, index-only, the covering projection), their plan premises, and the leak arms, including one through each vectorized node |

## 131. test_type_matrix.py: types the differential matrix never stored answer as heap

The pytest twin of `test/type_matrix.sh` (#1341), every check by the shell's name. The encoder has fast paths for fixed widths 1, 2, 4 and 8, and every other width and every varlena takes a generic path. This matrix stores what no test had: by-reference fixed widths 6, 8, 12, 16 and 64 (macaddr, macaddr8, timetz, point, name), the 1-byte `"char"`, time, money, inet, cidr, bit, varbit, tsvector, xml, json, an enum, a composite, a domain, and numeric NaN and ±Infinity with timestamp and date ±infinity. 12000 rows in row groups of 1000, each column NULL on its own pattern, are generated once into heap and copied. Every row as text, each column's counts, a range on every ordered column with literal bounds read from heap, json, point and tsvector matches, the special-value aggregates, and the composite's fields must all equal heap's.

### Every test

| test | what it holds |
| --- | --- |
| `test_types_the_matrix_never_stored_answer_as_heap` | the 21 columns against heap: rows, counts, sixteen ranges, the three non-btree matches, and the special values |
5 changes: 4 additions & 1 deletion test/pytest/expected_tests.txt
Original file line number Diff line number Diff line change
Expand Up @@ -724,4 +724,7 @@ guard_tests 438
# cluster_tests 599 -> 600 for #1341's test_savepoint_flush.py, one test, by collection
# cluster_tests 600 -> 601 for #1341's test_rls_query_paths.py, one test, by collection
# over the cluster files on this tree.
cluster_tests 601
# cluster_tests 601 -> 602 for #1341's test_type_matrix.py, one test, by collection
# cluster_tests 602 -> 603 for #1435's test_a_where_with_no_column_in_it_is_applied in
# test_native_agg.py, by collection over the cluster files on this tree.
cluster_tests 603
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 @@ -164,6 +164,7 @@
"tier_partition",
"timeseries_recipes",
"ttl_expire",
"type_matrix",
"vacuum_lock_privilege",
"validity_elision",
"vm_privilege",
Expand Down
Loading
Loading