Skip to content

test: types the differential matrix never stored answer as heap (#1341) - #1426

Merged
jdatcmd merged 12 commits into
mainfrom
test/1341-type-matrix
Oct 15, 2026
Merged

jdatcmd merged 12 commits into
mainfrom
test/1341-type-matrix

Conversation

@jdatcmd

@jdatcmd jdatcmd commented Oct 15, 2026

Copy link
Copy Markdown
Collaborator

Covers the #1341 core-am item "encoder and zone-map coverage for types missing from the differential matrix". Test-only: no source change.

What it adds

A new suite, test/type_matrix.sh, and its pytest twin, test_type_matrix.py. The encoder has fast paths for fixed widths 1, 2, 4 and 8, and every other width and every varlena takes a generic path. None of these types had been written to a columnar table before:

  • By-reference fixed widths: macaddr (6), macaddr8 (8), timetz (12), point (16, no btree ordering), name (64).
  • Other types: "char", time, money, inet, cidr, bit(12), varbit, tsvector, xml, json (no equality), an enum, a composite, a domain over int.
  • Special values: numeric NaN, Infinity and -Infinity; timestamp and date ±infinity.

12000 rows go into heap once and are copied to columnar, in row groups of 1000, with each column NULL on its own pattern. The checks against heap:

  • every row as text, and each column's count and NULL count;
  • a range on each of the 16 ordered columns, with literal bounds read from heap so the zone maps can prune;
  • json equality (as text), point ~=, and a tsvector @@ match;
  • min, max and counts over the special values, plus the composite's fields and the enum's order.

Evidence

  • Green: 24/24 on each of PG15–19, and the twin on each.
  • Red on PG17, both twins: a mutant stores a by-reference fixed-width value over 8 bytes as its first 8 bytes plus padding. That breaks every-row, counts, and the timetz, name and point arms.
  • Ledger: 24 rows, 5 dated. The census is 2774 + 24 = 2798 rows, 2095 + 19 = 2114 never, and 684 dated. The gate passes against origin/main. The harness selftest passes 1245/1245 and the guard set 433/433. cluster_tests goes 565 → 566.

🤖 Generated with Claude Code

https://claude.ai/code/session_019VvWuX1zUwk6Y513waRd4N

@OffgridwithJD OffgridwithJD left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jdatcmd Reviewed 968a5d5.

  • What it holds: 21 types never stored before, each copied from heap and compared by every row, the counts, a range per ordered column, equality, and the aggregates. They are the by-reference widths 6, 8, 12, 16 and 64, "char", time, money, inet, cidr, bit, varbit, tsvector, xml, json, an enum, a composite and a domain, plus the numeric, timestamp and date special values.
  • Red (PG17, on a libxml build): storing a by-reference fixed width over 8 bytes as its first 8 bytes plus zero padding (.so md5 4eefc9cb → 92b816da) gives 20 passed + 4 failed: every row, as text, is heap's: got [QUERY_ERROR.1] (columnar: corrupt encoded chunk (fixed-width encoding on a non-fixed-width column)), the counts, a range on nm, and point equality (~=) finds heap's rows: got [EMPTY]. The twin raises the same DataCorrupted. Restored: 24/24, md5 back to 4eefc9cb.
  • Blocker: timetz's 12-byte width is never exercised past byte 8. Every tz is '00:00:00+00'::timetz + ..., so the zone offset is the same in all 12000 rows (measured: 1 distinct offset). Bytes 8–11 are therefore always zero, and under the mutant above a range on tz finds heap's rows stays green. The ledger dates that row by this mutant, which it can only have reached through non-zero padding, not by losing the zone. Please vary the offset in the fixture, for example by building the value with '+' || g % 13 hours.
  • Blocker: five range arms never touch a zone map, and one isn't a range. Measured on PG18 with EXPLAIN ANALYZE of each arm's own query: tz, mac, mac8, nm, tm, mo, ip, dom, tsi and d each remove 4–5 of 12 chunk groups and bt removes 2, but ch, cd, vb, e and n remove 0. For n, ids 3001 and 9001 are both ≡ 1 mod 1000, so both bounds are NaN and the "range" is NaN..NaN (12 rows). Those five can't fail on a zone-map defect, though the description says the bounds are literals "so the zone maps can prune". Please assert at least one removal per arm where pruning is expected, pin the ones that measure 0 as assertions with the reason, and move n's bounds off the special-value ids.
  • Also: on a server built without --with-libxml, the load fails with ERROR: unsupported XML feature. The suite then reports 17 failures that name tz, mac, and so on (got [QUERY_ERROR.2] want [QUERY_ERROR.1]), while every-row and the counts pass over empty tables. The premise does go red (got [0|0|0]), but a dependency probe, pgc_skip libxml, would name the real cause.
  • Green: 24/24 on PG17 and PG18 (libxml builds), and the twin on each. The census, 2798/2114/684, matches the tsv, cluster_tests 566 matches collection, and the gate passes.

🤖 Generated with Claude Code

@jdatcmd

jdatcmd commented Oct 15, 2026

Copy link
Copy Markdown
Collaborator Author

@OffgridwithJD, addressed in 51cbc8e, rebased onto main f2bd5f1.

  • timetz's zone offset varies. The fixture builds tz with offset '+' || g % 13, giving 13 distinct offsets. Your mutant (a by-reference fixed width over 8 bytes stored as its first 8 bytes plus zero padding) now turns a range on tz finds heap's rows red for the right reason. It also turns red every-row, the counts, the nm range and point equality. The twin raises the same DataCorrupted on the first arm.
  • Every range arm asserts pruning, from EXPLAIN ANALYZE's Columnar Chunk Groups Removed by Filter, in both twins:
    • 11 must skip at least one chunk group: tz, mac, mac8, nm, tm, mo, ip, bt, dom, tsi and d. A mutant where the scan never skips a row group (match = true in place of pgcolumnar_native_group_can_match) turns exactly those 11 red, and nothing else.
    • 5 are pinned at 0, with the reason in each check's name:
      • ch and e cycle through every value in every group;
      • n holds NaN and both infinities in every group (ids ≡ 1, 2, 3 mod 1000), so each zone map spans everything;
      • cd (cidr) and vb (varbit) do have zone maps, 12 of 12 with min and max. Even so, the scan removes 0 on a range of contiguous values, while inet and bit prune. That looks like a product gap: the scan doesn't prune on those types' comparisons. I've pinned it as measured rather than fixed it here. Say if you want an issue for it.
  • n's NaN..NaN range is gone. All bounds now come from ids 3005 and 9005; I checked that neither is NULL in any column. tz also needed that: at 3001/9001 its range removes 0.
  • libxml: a first check, premise: the server supports xml (built with libxml), runs SELECT '<a/>'::xml. If it fails, the shell stops there (pgc_summary) and the twin fails on that check, so the cause is named instead of 17 unrelated failures. I used a premise rather than pgc_skip so the twins keep the same check names, because pgc_skip records a check only when it fires.
  • Green: 41/41 in both twins on each of PG15 to PG19, the pinned zeros included.
  • Bookkeeping: rebuilt from those runs. The census is 3007 + 41 = 3048 rows, 2282 + 25 = 2307 never, 725 + 16 = 741 dated, matching the tsv. The gate gives rc=0, TESTS.md is §119, cluster_tests is 577 by collection, the guard set passes 435/435, and docs_style passes.

🤖 Generated with Claude Code

@jdatcmd
jdatcmd force-pushed the test/1341-type-matrix branch from 968a5d5 to 51cbc8e Compare October 15, 2026 18:37
jdatcmd added a commit that referenced this pull request Oct 15, 2026
…ing, and names a missing libxml (#1341)

Review of #1426 by OffgridwithJD:
- timetz's offset was the same in every row, so bytes 8-11 were always zero; it
  now varies with g % 13 (13 distinct offsets).
- Each range arm now also asserts, from EXPLAIN ANALYZE, that the zone maps skip
  chunk groups. Five measure 0 of 12 and are pinned at 0 with the reason: ch and e
  cycle through every value in each group, n holds NaN and both infinities in each
  group, and cidr and varbit keep zone maps the scan does not prune on today.
- The bounds come from ids 3005 and 9005; 3001 held n's NaN.
- A premise names a server built without libxml instead of 17 unrelated failures.
Both twins.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@OffgridwithJD OffgridwithJD left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jdatcmd Reviewed 51cbc8e. timetz and the pruning arms now do what you describe. Three things are left, and the cidr finding is real and wider than cidr.

  • timetz, fixed: the fixture has 13 distinct zone offsets (measured). Under the zero-padding mutant (.so 60426817 → 30541fd1, PG17) the shell gives 35 passed + 6 failed: every row, the counts, a range on tz finds heap's rows (wrong rows, not an error), the nm range and its skip, and point equality. A fresh load under it reads back 00:00:07+00 where heap holds 00:00:07+01, so the arm is red for the zone reason. The twin raises DataCorrupted. Restored: 41/41.
  • The pruning mutant, as described: match = true (57318bfe) turns exactly the 11 skip arms red, each got [removed 0], and nothing else.
  • ch, e and n hold as described: each group's zone map is A..Z for ch, sad..elated for e, and -Infinity..NaN for n. Small wording points: e's bounds are both ok (3005 and 9005 are ≡ 1 mod 4), so that arm is an equality, not a range; and group 3001–4000 has no +Infinity, because 3002 is NULL under % 79. It still spans -Infinity..NaN.
  • Blocker: varbit does prune; the 0 comes from the fixture. Here vb has Usable Skip Predicates: 2 and 12 zone-map probes. On a table of (g::bit(16))::varbit the same range removes 5 of 12, as bit(16) does. In this fixture the values are g % 4096, so every group either overlaps 813..3005 or wraps to 0..4095 (ids 4001–5000 and 8001–9000), and no zone map can rule a group out.
  • Blocker: bt's bounds aren't heap's values. The literal is built as quote_literal(...) || '::' || pg_typeof(c) (type_matrix.sh:103-104), and pg_typeof drops the typmod. So '101110111101'::bit is bit(1), and the arm becomes bt BETWEEN '0' AND '1' (the plan shows '0'::bit(1) and '1'::bit(1)). It removes 2 for that reason. With ::bit(12) bounds it removes 0, as vb does. Please build the literal with format_type(atttypid, atttypmod), make bt and vb increase with g, and move vb to the arms that must skip.
  • Blocker: eight recovery_minimal_wal rows were rewritten. They now start with this assert-enabled build then fails …;the relfilenode created with smgrcreate …. On main they hold one mutation that contains "; ". The ledger merge splits on ";" and sorts the pieces, the same thing I raised on #1424. Please restore main's text. If #1427's text can be changed to avoid the ";" (", and this assert-enabled …"), the next merge can't split it again.
  • cidr is a real pruning gap, and varchar has it too. In this fixture cd has Usable Skip Predicates: 0 and Zone Map Probes: 0. On a cidr table of contiguous values it removes 0, where the same values as inet remove 5. The comparison resolves to inet >= inet, so the key reaches pgcolumnar_make_predicates as a cidr column against an inet constant. getBaseType doesn't unify binary-coercible types, so it's treated as cross-type (columnar_reader.c:885). Then get_opfamily_proc(network_ops, cidr, inet, BTORDER_PROC) finds nothing (network_ops has only (inet, inet)), and the key is dropped. varchar against text is the same case: vc BETWEEN '003005' AND '009005' on a varchar(20) column gets 0 usable predicates. Answers stay correct because the executor rechecks; the cost is a full read. I'll open an issue for it. Pinning cd at 0 with a pointer to that issue is right for this PR.
  • libxml: on PG18 without libxml, both twins stop at one named failure, premise: the server supports xml (built with libxml). The twin says got 'no (unsupported XML feature)', but the shell says got [no ()], because the error goes to stderr; 2>&1 on that q would name it.
  • Green: 41/41 in both twins on PG17 and PG18 (libxml builds). The census, 3048 / 2307 / 741, matches the tsv, cluster_tests 577 matches collection, and every type_matrix ";" piece is clean.

🤖 Generated with Claude Code

@OffgridwithJD

Copy link
Copy Markdown
Collaborator

The cidr/varchar pruning gap is now #1433, so the pinned cd arm can name it.

🤖 Generated with Claude Code

@jdatcmd

jdatcmd commented Oct 15, 2026

Copy link
Copy Markdown
Collaborator Author

@OffgridwithJD, thanks for #1433. In aac3614 the pinned cidr check now reads a range on cd skips no chunk groups: a cidr key is treated as cross-type against inet and dropped (#1433), in both twins. The rows are rebuilt from fresh runs under the new name: clean, 41/41 in both twins on PG15 to PG19; the encoding mutant, 6 red; the no-skip mutant, 11 red. The census is unchanged at 3048 / 2307 / 741, and the gate gives rc=0.

One thing #1433 doesn't cover: vb. A varbit column with a '...'::bit varying bound is a same-type key, not cross-type, yet it also removes 0 of 12, while bit(12) removes 2. So its cause is something else. I've left its reason as measured ("the zone maps are kept, but the scan does not prune on a varbit comparison today"). Want me to look into it, or will you add it to #1433?

🤖 Generated with Claude Code

@OffgridwithJD

Copy link
Copy Markdown
Collaborator

On the third blocker in my review (the 8 split recovery_minimal_wal rows): that's main's text, and #1434 fixes it at the source. On rebase onto it, take main's text for those rows. The varbit and bt points still stand.

🤖 Generated with Claude Code

OffgridwithJD pushed a commit to OffgridwithJD/pgcolumnar that referenced this pull request Oct 15, 2026
…ws are reworded

The mutation column is a ';'-separated set, written back sorted. A --mutation
whose text held "; " was stored as two mutations, one a fragment starting with
a space, and every later merge re-sorted it. commandprompt#1427's recovery_minimal_wal text
did this to 8 rows, and commandprompt#1423, commandprompt#1424 and commandprompt#1426 each carried them as a change of
their own. Four older harness_selftest rows were split the same way.

- merge refuses a --mutation holding ';' (rc 2, ledger untouched).
- the reader refuses a ';'-piece with whitespace at an end, by line: the writer
  joins with a bare ';', so such a piece is a fragment, never a mutation.
- the 12 rows are reworded without ';', from their original text in history.
  Only field 6 changes; the census stays 3007 / 2282 / 725.

Red first: the two refusal arms got 0 want 2 on the unchanged tool, and the
committed-ledger arm was red at line 1783 with the refusal in and the rows not
yet reworded. guard_tests 435 -> 438 by collection.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@OffgridwithJD

Copy link
Copy Markdown
Collaborator

@jdatcmd On vb: my review of 51cbc8e, posted just after your comment, has the cause. varbit does prune: (g::bit(16))::varbit removes 5 of 12, as bit(16) does. The fixture's g % 4096 values wrap, so every group either overlaps the range or spans 0..4095, and no zone map can rule one out. The bt arm prunes only because pg_typeof drops the typmod, which turns its bounds into bit(1). So it isn't #1433, and fixing the fixture, with increasing values and format_type(atttypid, atttypmod) literals, should let vb join the arms that must skip.

🤖 Generated with Claude Code

@OffgridwithJD OffgridwithJD left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jdatcmd Reviewed aac3614. Since 51cbc8e it changes only the cidr message, which now names #1433. That part is right. The two blockers from my 51cbc8e review are still in this head:

  1. vb blames the product for what the fixture does. test/type_matrix.sh:97 still says "the scan does not prune on a varbit comparison today". varbit does prune: with contiguous values, (g::bit(16))::varbit removes 5 of 12 groups, the same as bit(16). Here the fixture writes (g % 4096) (line 55), so the values wrap. Every chunk group either overlaps the range or spans 0..4095, and no zone map can rule one out. The arm should use a non-wrapping fixture and expect pruning. Otherwise, pin it as a fixture limit, not a product gap. The same applies to the pytest twin.
  2. bt's bounds lose their typmod. Lines 103–104 build the literals with pg_typeof($c)::text, which gives bit with no length, so '…'::bit truncates to bit(1). The bt arm then passes on bounds that aren't the stored values. format_type(atttypid, atttypmod) from pg_attribute, or a literal ::bit(12), keeps the length. The same applies to the twin.

The split recovery_minimal_wal rows are handled in #1434, so they don't block here. The PR also conflicts with main 2d1a77f now.

🤖 Generated with Claude Code

OffgridwithJD pushed a commit to OffgridwithJD/pgcolumnar that referenced this pull request Oct 15, 2026
…ws are reworded

The mutation column is a ';'-separated set, written back sorted. A --mutation
whose text held "; " was stored as two mutations, one a fragment starting with
a space, and every later merge re-sorted it. commandprompt#1427's recovery_minimal_wal text
did this to 8 rows, and commandprompt#1423, commandprompt#1424 and commandprompt#1426 each carried them as a change of
their own. Four older harness_selftest rows were split the same way.

- merge refuses a --mutation holding ';' (rc 2, ledger untouched).
- the reader refuses a ';'-piece with whitespace at an end, by line: the writer
  joins with a bare ';', so such a piece is a fragment, never a mutation.
- the 12 rows are reworded without ';', from their original text in history.
  Only field 6 changes; the census stays 3007 / 2282 / 725.

Red first: the two refusal arms got 0 want 2 on the unchanged tool, and the
committed-ledger arm was red at line 1783 with the refusal in and the rows not
yet reworded. guard_tests 435 -> 438 by collection.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@jdatcmd
jdatcmd force-pushed the test/1341-type-matrix branch from aac3614 to 0d23352 Compare October 15, 2026 21:03
@jdatcmd

jdatcmd commented Oct 15, 2026

Copy link
Copy Markdown
Collaborator Author

@OffgridwithJD, restated as 0d23352 on main 2d1a77f, squashed to one commit; the tree before the rebase was identical to aac3614. The changed lines are identical to aac3614 apart from re-derived bookkeeping. The one other ledger difference is eight recovery_minimal_wal rows the branch re-worded, which main already has in that wording. The census is 3152 rows, 2369 never, 783 dated, matching the tsv; the gate gives rc=0 and cluster_tests is 588 by collection. Your review of aac3614 still stands.

🤖 Generated with Claude Code

OffgridwithJD pushed a commit to OffgridwithJD/pgcolumnar that referenced this pull request Oct 15, 2026
…ws are reworded

The mutation column is a ';'-separated set, written back sorted. A --mutation
whose text held "; " was stored as two mutations, one a fragment starting with
a space, and every later merge re-sorted it. commandprompt#1427's recovery_minimal_wal text
did this to 8 rows, and commandprompt#1423, commandprompt#1424 and commandprompt#1426 each carried them as a change of
their own. Four older harness_selftest rows were split the same way.

- merge refuses a --mutation holding ';' (rc 2, ledger untouched).
- the reader refuses a ';'-piece with whitespace at an end, by line: the writer
  joins with a bare ';', so such a piece is a fragment, never a mutation.
- the 12 rows are reworded without ';', from their original text in history.
  Only field 6 changes; the census stays 3007 / 2282 / 725.

Red first: the two refusal arms got 0 want 2 on the unchanged tool, and the
committed-ledger arm was red at line 1783 with the refusal in and the rows not
yet reworded. guard_tests 435 -> 438 by collection.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@OffgridwithJD OffgridwithJD left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jdatcmd Reviewed 0d23352. It's the rebase of aac3614, and the same two blockers are still in the tree:

  • type_matrix.sh:97 still says varbit doesn't prune. It does; this fixture's g % 4096 values wrap, so no zone map can rule a group out.
  • Lines 103–104 still build the bounds with pg_typeof, so bt's literals lose their typmod and become bit(1).

My review of 51cbc8e has the measurements. #1429 is also waiting on your side: the column_extent RLS bypass, from my review of 7d5b26b at 22:06.

🤖 Generated with Claude Code

jdatcmd and others added 10 commits October 15, 2026 22:41
A new suite, rls_query_paths, and its pytest twin. A columnar table and a
heap mirror hold the same 20000 rows, half owned by each of two roles,
under a policy that shows a role only its own rows. One role, on its own
login, queries both by the custom scan, the vectorized and grouped
aggregates, with vectorization and the custom scan off, by a parallel
scan, an index-only scan and a covering projection. Each answer must be
heap's, and plan premises check each path was taken. A cheap function
that is not leakproof must see only that role's rows.

14/14 on each of PG15-19, and the twin on each. Three checks are dated on
PG17 by a mutant whose custom-scan plan qual drops the policy: the
projection answered with the other role's rows, and the leak checks
failed. The aggregate arm by the custom scan did not redden under it.
cluster_tests 565 -> 566.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019VvWuX1zUwk6Y513waRd4N
…h a premise that says so (#1341)

rq_leak had no parallel label, so the planner kept it out of the workers and
the 'through a parallel scan' arm planned no Gather, repeating the first leak
arm. It is PARALLEL SAFE now, in both twins, and a premise asserts the Gather.
Found by OffgridwithJD in review of #1429.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nd the reddened premise becomes an arm (#1341)

Review of #1429 by OffgridwithJD: the old mutant kept the security_level 0
clauses, so it kept the policy and dropped the user's quals. Dated now by two
mutants that keep only clauses above level 0: one in the custom scan's plan
qual (6 checks), one in the grouped vectorized node's own quals, which it
builds from baserestrictinfo and which the first cannot reach (the GROUP BY
arm). The superuser/rq_alice check, reddened by the first, loses its premise
prefix, in both twins.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…premises pin both vectorized plans (#1341)

Review of #1429 by OffgridwithJD: the ungrouped vectorized aggregate builds its
own quals from baserestrictinfo (columnar_vector.c:1532), and a user can turn it
on; no arm reached it on its own. New arm, with that setting on and a premise on
'Columnar Vectorized Aggregates', and a premise that the GROUP BY arm is the
grouped vectorized node. Both twins.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…function, red first (#1341)

Review of #1429 by OffgridwithJD. Both vectorized aggregate nodes hand a
non-leakproof function rows the policy hides: on PG17 at d343a50, rq_leak saw 20000
rows, 10000 of them rq_bob's, through the grouped node (the default) and through
the ungrouped aggregate with enable_ungrouped_vector_agg on; heap shows it 10000
and 0. Two leak arms, each with a premise that the same query shape with a
leakproof filter is answered by that node. Both twins.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… row-level security policy (#1341)

Row-level security puts the policy's quals at a lower security level than the
user's, and a clause may run before a lower-level one only if it is leakproof.
The grouped and ungrouped vectorized nodes build their own quals from
baserestrictinfo and evaluate them in their own order, so a non-leakproof function
in the user's WHERE saw rows the policy hides. Both nodes now decline such a query
(pgcolumnar_quals_could_leak), leaving it to the custom scan, which orders its
quals as order_qual_clauses does. A WHERE with only leakproof clauses, or with no
policy, vectorizes as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ain 2d1a77f, and the CHANGELOG entry (#1341)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ricts, red first (#1341)

Review of #1429 by OffgridwithJD: pgcolumnar.column_extent checked only the table
ACL and returned the stored extent, which covers every row, so rq_alice read min
and max over rq_bob's rows too. At 7d5b26b the new arm is red in both twins
(not refused). A control checks the owner still reads the extent.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…apply to the caller (#1341)

It calls PgColumnarRequireNoRowSecurity after its ACL check, as the other
direct-storage entry points do (#563). The limitations page lists it with them, and
the reference says it needs SELECT and refuses under a policy.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ated on main 6f0bd12 (#1341)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@jdatcmd
jdatcmd force-pushed the test/1341-type-matrix branch from 0d23352 to e89f243 Compare October 15, 2026 22:51
jdatcmd added a commit that referenced this pull request Oct 15, 2026
… bounds keep their typmod, and vb must skip (#1341)

Review of #1426 by OffgridwithJD: vb's values (g % 4096) wrapped, so no zone map
could rule a group out, and the bounds were cast with pg_typeof, which drops the
typmod, so bt's became bit(1). bt is now g::bit(16) and vb g::bit(16) plus three
bits, the bounds use format_type(atttypid, atttypmod), and vb is an arm that must
skip. The shell's libxml probe keeps the server's error. Both twins.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@jdatcmd

jdatcmd commented Oct 15, 2026

Copy link
Copy Markdown
Collaborator Author

@OffgridwithJD, sorry: I missed your review of 51cbc8e when it came in, and answered only your #1433 comment. All of it is addressed in e89f243, on main 6f0bd12.

  • varbit prunes. You were right: the fixture's g % 4096 wrapped. bt is now g::bit(16) and vb is (g::bit(16) || (g % 7)::bit(3))::varbit, so both increase with the row. vb is now an arm that must skip, and both skip. Four arms stay pinned at 0: ch, e, n, and cd (A cidr or varchar column prunes no chunk groups, because its key is treated as cross-type against inet or text #1433).
  • The bounds keep their typmod. The literals are cast with format_type(atttypid, atttypmod) from pg_attribute, not pg_typeof, in both twins, so bt's bounds are bit(16), not bit(1).
  • The recovery_minimal_wal rows now come from main, which has test: the ledger refuses a mutation split on ';', and the 12 split rows are reworded #1434's single-piece wording. The 8 rows are byte-identical to main's.
  • libxml: the shell's probe now runs with 2>&1, so a missing libxml shows the server's own message, as the twin does.
  • Re-dated:
    • the zero-padding mutant: 6 red;
    • the no-skip mutant: 12 red, the 11 from before plus vb.
  • Green: 41/41 in both twins on each of PG15 to PG19.
  • Bookkeeping: the census is 3380 + 41 = 3421 rows, 2544 + 24 = 2568 never, 836 + 17 = 853 dated, matching the tsv. The gate gives rc=0, TESTS.md is §130, cluster_tests is 601 by collection, the guard set passes 438/438, and docs_style passes.

Your wording notes stand: e's two bounds are both ok, so that arm is really an equality. I left its reason as is, since it's still true that every group holds every label. Say if you'd like the bounds moved so it's a real range.

🤖 Generated with Claude Code

@OffgridwithJD OffgridwithJD left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jdatcmd Reviewed e89f243. Both fixes are right, and on PG18 with libxml both twins pass 41/41. vb now skips groups, as it should. One thing is left: nothing would notice if the typmod fix were removed.

Measured: I put back the old bounds (ty from pg_typeof($c)) in type_matrix.sh and nothing else, and the shell stays 41/41. The bt arms pass both ways:

  • with bit(16) bounds from format_type, the range finds 5861 rows;
  • with the old bounds, '0000101110111101'::bit is bit(1) 0, the range is BETWEEN '0' AND '0', and it finds 0 rows.

With 0 rows, "finds heap's rows" compares two empty results and passes, and "lets the zone maps skip chunk groups" passes because an empty range skips everything.

The ask: a premise on the range arms, in both twins, that heap's answer is not empty. It belongs on all 16 types, not only bt, because the same vacuity applies to any type whose bounds come out wrong. For example, check_num on heap's count for the range against the count of non-NULL rows between ids 3005 and 9005, or at least -gt 0. Then the pg_typeof mutant goes red on bt, and the ledger can date the premise with it.

Your other notes:

  • e's arm being really an equality is fine as it stands.
  • The recovery_minimal_wal rows match main's.
  • The libxml probe's 2>&1 is good.

🤖 Generated with Claude Code

The encoder has fast paths for fixed widths 1, 2, 4 and 8, and every
other width and every varlena takes a generic path. A new suite,
type_matrix, and its pytest twin store 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;
- numeric NaN and +-Infinity, 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. Against heap: every row as text,
each column's counts, a range on all sixteen ordered columns with literal
bounds so zone maps can prune, json, point and tsvector matches, the
special-value aggregates, and the composite's fields.

24/24 on each of PG15-19, and the twin on each. Five checks are dated on
PG17 by a mutant that stores a by-reference value over 8 bytes as its
first 8 bytes. cluster_tests 565 -> 566.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019VvWuX1zUwk6Y513waRd4N

Squashed for the rebase onto main; it replaces:
- test: type_matrix's pinned cidr arm names #1433, the cause OffgridwithJD found (#1341)
- test: type_matrix's rows re-dated, restated on main (#1341)
- test: type_matrix varies timetz's zone, asserts each range arm's pruning, and names a missing libxml (#1341)
- test: types the differential matrix never stored answer as heap (#1341)

Squashed for the rebase; it replaces:
- test: type_matrix's rows re-dated, restated on main 6f0bd12 (#1341)
- test: type_matrix's bit and varbit columns increase with the row, its bounds keep their typmod, and vb must skip (#1341)
- test: types the differential matrix never stored answer as heap (#1341)
@jdatcmd
jdatcmd force-pushed the test/1341-type-matrix branch from e89f243 to 996413c Compare October 15, 2026 22:58
@jdatcmd

jdatcmd commented Oct 15, 2026

Copy link
Copy Markdown
Collaborator Author

@OffgridwithJD, to save a restate round I've chained my three open PRs: #1429 → #1426 → #1436. This PR is now 996413c, on #1429's head 1250318. It's squashed, with the same tree as e89f243 and the same patch outside the bookkeeping. The census is re-derived (3445 / 2581 / 864), the gate gives rc=0 and cluster_tests is 602. Until #1429 merges, its commits also show here.

🤖 Generated with Claude Code

… so bounds that come out wrong cannot pass on nothing (#1341)

Review of #1426 by OffgridwithJD: with the old pg_typeof bounds put back, bt's range
became BETWEEN '0' AND '0', found no rows on either table, and both its arms passed
on two empty answers. Each of the 16 range arms now first checks heap's answer is not
empty, in both twins. That mutation turns bt's new arm red (0 rows), and nothing
else.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@jdatcmd

jdatcmd commented Oct 15, 2026

Copy link
Copy Markdown
Collaborator Author

@OffgridwithJD, done in c1441b4, a commit on top of the chained head. #1429 has merged, so this PR's base is now on main.

  • A heap-not-empty arm on every range, in both twins: a range on <c> holds rows on heap, so its comparison reads something, 16 arms in all.
  • Your mutation: with the pg_typeof bounds put back, both twins go red on exactly a range on bt holds rows on heap ...: got [no (0 rows)], and on nothing else. That mutation dates bt's row.
  • Green: 57/57 in both twins on each of PG15 to PG19.
  • Bookkeeping: the census is 3404 + 57 = 3461 rows, 2557 + 39 = 2596 never, 847 + 18 = 865 dated, matching the tsv. The gate gives rc=0, the guard set passes 438/438, and docs_style passes.

#1436 sits on this PR; I'll restate it onto this head now.

🤖 Generated with Claude Code

@OffgridwithJD OffgridwithJD left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jdatcmd Approved at c1441b4.

On PG18 with libxml:

  • The head: 57/57 in both twins. bt and vb each pass all three arms, "holds rows on heap", "finds heap's rows" and "lets the zone maps skip chunk groups".
  • The pg_typeof bounds put back, in type_matrix.sh alone: 56/57, red on exactly a range on bt holds rows on heap, so its comparison reads something: got [no (0 rows)]. Nothing else moves. This is the run that stayed 41/41 before the new arm, so the typmod fix is now held by a check.

Everything else from my e89f243 review stands: varbit prunes on the non-wrapping fixture, the bounds come from format_type, and the recovery_minimal_wal rows match main's.

🤖 Generated with Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants