diff --git a/CHANGELOG.md b/CHANGELOG.md index cae5839a..d346b3a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,15 @@ true until the next version shipped. ### Added +- Exact zone-map boundary coverage now lives in matching shell and pytest tests + (#831). + + The `<=` and `>=` arms put the constant exactly at a row-group minimum or + maximum and compare returned rows with a heap twin. The `<`, `>`, and `=` + mirrors assert groups removed with bloom disabled, so a conservative pruning + regression cannot hide behind a correct answer. Each of the five one-token + strategy mutations was proved to fail its corresponding assertion. + - The test harness refuses to measure a binary that was not built from the source under test. diff --git a/test/pytest/TESTS.md b/test/pytest/TESTS.md index 576c3c88..06f4cb36 100644 --- a/test/pytest/TESTS.md +++ b/test/pytest/TESTS.md @@ -4,7 +4,7 @@ Reference for anyone reading, running, or adding to `test/pytest/`. The design a the decisions behind the harness are in `design/ISSUE_432_PYTEST_HARNESS.md`. This file covers the tests themselves. -**120 tests in 8 files.** One hundred and five of them test the harness rather than the +**121 tests in 9 files.** One hundred and six of them test the harness rather than the product, and they come first, because a harness that can report a false green makes every other result in this directory worthless. @@ -34,9 +34,10 @@ behaviour, the source of that number is named. - [8. test_native_projection.py: the ported suite](#8-test_native_projectionpy-the-ported-suite) - [9. test_ordered.py: the ordered oracle](#9-test_orderedpy-the-ordered-oracle) - [10. test_runshape.py: the shape of the run itself](#10-test_runshapepy-the-shape-of-the-run-itself) -- [11. Adding a test](#11-adding-a-test) -- [12. What this corpus does NOT yet refuse](#12-what-this-corpus-does-not-yet-refuse) -- [13. Traps this corpus records](#13-traps-this-corpus-records) +- [11. test_zonemap_boundaries.py: exact boundaries](#11-test_zonemap_boundariespy-exact-boundaries) +- [12. Adding a test](#12-adding-a-test) +- [13. What this corpus does NOT yet refuse](#13-what-this-corpus-does-not-yet-refuse) +- [14. Traps this corpus records](#14-traps-this-corpus-records) ## 1. How to read a test in here @@ -861,7 +862,24 @@ The empty-parametrize refusal carries its own message rather than folding into t bare-skip refusal. When a corpus glob matches nothing, the cause the reader needs to see is the corpus, not the marker. -## 11. Adding a test +## 11. test_zonemap_boundaries.py: exact boundaries + +### `test_exact_zonemap_boundaries` + +Pairs with `test/zonemap_boundaries.sh`. Two monotonic 1,000-row groups put +`1001` exactly at the second group's minimum and `1000` exactly at the first +group's maximum. Heap-row comparisons pin that `<= 1001` and `>= 1000` keep +their boundary rows. Work-done counters, with bloom disabled, pin the +correctness-preserving cases: `< 1001`, `> 1000`, and `= 1001` each remove one +group. + +The five one-token strategy mutations make the corresponding assertion fail: +`<=` and `>=` lose one row, while `<`, `>`, and `=` remain row-correct but +remove no group. This distinguishes correctness coverage from +pruning-effectiveness coverage rather than relying on incidental fixtures +elsewhere in the matrix. + +## 12. Adding a test 0. **Write it twice.** Every test in this tree ships as a `.sh` suite and a pytest test **in the same change** (jd, 2026-09-09). Not ported later, not one or the @@ -888,7 +906,7 @@ see is the corpus, not the marker. failed the selftest on both majors of the matrix, which is how it was found. A new directory under `test/` inherits every rule the old ones follow. -## 12. What this corpus does NOT yet refuse +## 13. What this corpus does NOT yet refuse `VACUITY_MODES.md` is the inventory: 79 ways a pytest harness can report a pass while asserting nothing, 73 of them demonstrated by an actual run. **This layer refuses 25 @@ -900,7 +918,7 @@ Read it before adding a test. The gaps most likely to affect a new test are that same family satisfies it, and that a write is not required to have written anything. Both are named there with the refusal each needs. -## 13. Traps this corpus records +## 14. Traps this corpus records Recorded because each one produced a confident wrong result before it was caught, and all are the same family as the defect the layer exists to prevent. diff --git a/test/pytest/test_zonemap_boundaries.py b/test/pytest/test_zonemap_boundaries.py new file mode 100644 index 00000000..03c2f53d --- /dev/null +++ b/test/pytest/test_zonemap_boundaries.py @@ -0,0 +1,82 @@ +"""Exact zone-map comparison boundaries and pruning effectiveness (#831).""" + + +def _nodes(plan): + for root in plan: + stack = [root["Plan"]] + while stack: + node = stack.pop() + yield node + stack.extend(node.get("Plans", ())) + + +def _plan(conn, qual): + with conn.cursor() as cur: + cur.execute("SET pgcolumnar.enable_bloom_filter=off") + cur.execute("SET pgcolumnar.enable_vectorization=off") + cur.execute( + "EXPLAIN (ANALYZE, FORMAT JSON, COSTS OFF, TIMING OFF, SUMMARY OFF) " + f"SELECT id FROM zb_c WHERE {qual}" + ) + return cur.fetchone()[0] + + +def _removed(plan): + return next( + node.get("Columnar Chunk Groups Removed by Filter", 0) + for node in _nodes(plan) + if "Columnar Chunk Groups Total" in node + ) + + +def _ids(conn, table, qual): + with conn.cursor() as cur: + cur.execute(f"SELECT id FROM {table} WHERE {qual} ORDER BY id") + return [row[0] for row in cur] + + +def test_exact_zonemap_boundaries(pgc_conn, expect): + with pgc_conn.cursor() as cur: + cur.execute("CREATE TABLE zb_h(id int, v int)") + cur.execute("CREATE TABLE zb_c(id int, v int) USING pgcolumnar") + cur.execute( + "SELECT pgcolumnar.set_options('zb_c', stripe_row_limit => 1000)" + ) + cur.execute("INSERT INTO zb_h SELECT g,g FROM generate_series(1,2000) g") + cur.execute("INSERT INTO zb_c SELECT * FROM zb_h") + cur.execute( + "SELECT count(*) FROM pgcolumnar.row_group " + "WHERE storage_id=pgcolumnar.get_storage_id('zb_c')" + ) + expect.num(cur.fetchone()[0], 2, "premise: fixture has two row groups") + + expect.num( + _removed(_plan(pgc_conn, "v < 1001")), 1, + "< excludes the group whose minimum equals the constant", + ) + expect.rows( + _ids(pgc_conn, "zb_c", "v <= 1001"), + _ids(pgc_conn, "zb_h", "v <= 1001"), + "<= keeps the row at a row-group minimum", + ) + expect.num( + _removed(_plan(pgc_conn, "v <= 1001")), 0, + "premise: <= at the second-group minimum removes no group", + ) + expect.rows( + _ids(pgc_conn, "zb_c", "v >= 1000"), + _ids(pgc_conn, "zb_h", "v >= 1000"), + ">= keeps the row at a row-group maximum", + ) + expect.num( + _removed(_plan(pgc_conn, "v >= 1000")), 0, + "premise: >= at the first-group maximum removes no group", + ) + expect.num( + _removed(_plan(pgc_conn, "v > 1000")), 1, + "> excludes the group whose maximum equals the constant", + ) + expect.num( + _removed(_plan(pgc_conn, "v = 1001")), 1, + "= excludes the group lying wholly below the constant", + ) diff --git a/test/run_all_versions.sh b/test/run_all_versions.sh index b6330593..a8c951da 100755 --- a/test/run_all_versions.sh +++ b/test/run_all_versions.sh @@ -286,6 +286,7 @@ SUITES=( wal_envelope write_fsst_compressed write_minmax_fastpath + zonemap_boundaries zonemap_cost zonemap_estimate_sample) diff --git a/test/zonemap_boundaries.sh b/test/zonemap_boundaries.sh new file mode 100755 index 00000000..ea1693ab --- /dev/null +++ b/test/zonemap_boundaries.sh @@ -0,0 +1,54 @@ +#!/usr/bin/env bash +# +# Exact zone-map comparison boundaries are intentional coverage (#831). +# +# Usage: test/zonemap_boundaries.sh [PG_CONFIG] + +set -uo pipefail +. "$(dirname "${BASH_SOURCE[0]}")/lib.sh" +pgc_setup "${1:-/usr/local/pg17/bin/pg_config}" + +psql_run "CREATE TABLE zb_h(id int, v int); + CREATE TABLE zb_c(id int, v int) USING pgcolumnar; + SELECT pgcolumnar.set_options('zb_c', stripe_row_limit => 1000); + INSERT INTO zb_h SELECT g,g FROM generate_series(1,2000) g; + INSERT INTO zb_c SELECT * FROM zb_h; + ANALYZE zb_h; ANALYZE zb_c;" >/dev/null + +check "premise: the boundary fixture has two row groups" \ + "$(q "SELECT count(*) FROM pgcolumnar.row_group + WHERE storage_id=pgcolumnar.get_storage_id('zb_c');")" "2" + +groups_removed() { + env PATH="$PGC_BINDIR:$PATH" psql -h 127.0.0.1 -p "$PGC_PORT" \ + -U postgres -d "$PGC_DB" -Atq \ + -c "SET pgcolumnar.enable_bloom_filter=off; + SET pgcolumnar.enable_vectorization=off; + EXPLAIN (ANALYZE, COSTS OFF, TIMING OFF, SUMMARY OFF) + SELECT id FROM zb_c WHERE $1;" 2>&1 | + sed -n 's/.*Columnar Chunk Groups Removed by Filter: \([0-9]*\).*/\1/p' | + head -1 +} + +check "< excludes the group whose minimum equals the constant" \ + "$(groups_removed 'v < 1001')" "1" +check_text "<= keeps the row at a row-group minimum" \ + "$(pgc_set_hash "SELECT id FROM zb_c WHERE v <= 1001")" \ + "$(pgc_set_hash "SELECT id FROM zb_h WHERE v <= 1001")" +check "premise: <= at the second-group minimum removes no group" \ + "$(groups_removed 'v <= 1001')" "0" + +check_text ">= keeps the row at a row-group maximum" \ + "$(pgc_set_hash "SELECT id FROM zb_c WHERE v >= 1000")" \ + "$(pgc_set_hash "SELECT id FROM zb_h WHERE v >= 1000")" +check "premise: >= at the first-group maximum removes no group" \ + "$(groups_removed 'v >= 1000')" "0" + +# These mutations remain row-correct, so only work-done counters can see them. +check "> excludes the group whose maximum equals the constant" \ + "$(groups_removed 'v > 1000')" "1" +check "= excludes the group lying wholly below the constant" \ + "$(groups_removed 'v = 1001')" "1" + +check "backend alive" "$(q 'SELECT 1')" "1" +pgc_summary