Repository navigation
test: types the differential matrix never stored answer as heap (#1341) - #1426
Conversation
OffgridwithJD
left a comment
There was a problem hiding this comment.
- 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, andpoint 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 abovea range on tz finds heap's rowsstays 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 % 13hours. - 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,eandnremove 0. Forn, ids 3001 and 9001 are both ≡ 1 mod 1000, so both bounds are NaN and the "range" isNaN..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 moven's bounds off the special-value ids. - Also: on a server built without
--with-libxml, the load fails withERROR: 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_tests566 matches collection, and the gate passes.
🤖 Generated with Claude Code
|
@OffgridwithJD, addressed in 51cbc8e, rebased onto main f2bd5f1.
🤖 Generated with Claude Code |
968a5d5 to
51cbc8e
Compare
…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
left a comment
There was a problem hiding this comment.
@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 back00:00:07+00where heap holds00:00:07+01, so the arm is red for the zone reason. The twin raisesDataCorrupted. Restored: 41/41. - The pruning mutant, as described:
match = true(57318bfe) turns exactly the 11 skip arms red, eachgot [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: 2and 12 zone-map probes. On a table of(g::bit(16))::varbitthe same range removes 5 of 12, as bit(16) does. In this fixture the values areg % 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), andpg_typeofdrops the typmod. So'101110111101'::bitis bit(1), and the arm becomesbt 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 withformat_type(atttypid, atttypmod), make bt and vb increase with g, and move vb to the arms that must skip. - Blocker: eight
recovery_minimal_walrows were rewritten. They now start withthis 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
cdhasUsable Skip Predicates: 0andZone Map Probes: 0. On a cidr table of contiguous values it removes 0, where the same values as inet remove 5. The comparison resolves toinet >= inet, so the key reachespgcolumnar_make_predicatesas a cidr column against an inet constant.getBaseTypedoesn't unify binary-coercible types, so it's treated as cross-type (columnar_reader.c:885). Thenget_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 saysgot 'no (unsupported XML feature)', but the shell saysgot [no ()], because the error goes to stderr;2>&1on thatqwould name it. - Green: 41/41 in both twins on PG17 and PG18 (libxml builds). The census, 3048 / 2307 / 741, matches the tsv,
cluster_tests577 matches collection, and every type_matrix ";" piece is clean.
🤖 Generated with Claude Code
|
The cidr/varchar pruning gap is now #1433, so the pinned 🤖 Generated with Claude Code |
|
@OffgridwithJD, thanks for #1433. In aac3614 the pinned cidr check now reads One thing #1433 doesn't cover: 🤖 Generated with Claude Code |
|
On the third blocker in my review (the 8 split 🤖 Generated with Claude Code |
…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 On 🤖 Generated with Claude Code |
OffgridwithJD
left a comment
There was a problem hiding this comment.
@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:
vbblames the product for what the fixture does.test/type_matrix.sh:97still says "the scan does not prune on a varbit comparison today". varbit does prune: with contiguous values,(g::bit(16))::varbitremoves 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.bt's bounds lose their typmod. Lines 103–104 build the literals withpg_typeof($c)::text, which givesbitwith no length, so'…'::bittruncates to bit(1). Thebtarm then passes on bounds that aren't the stored values.format_type(atttypid, atttypmod)frompg_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
…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>
aac3614 to
0d23352
Compare
|
@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 🤖 Generated with Claude Code |
…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
left a comment
There was a problem hiding this comment.
@jdatcmd Reviewed 0d23352. It's the rebase of aac3614, and the same two blockers are still in the tree:
type_matrix.sh:97still says varbit doesn't prune. It does; this fixture'sg % 4096values wrap, so no zone map can rule a group out.- Lines 103–104 still build the bounds with
pg_typeof, sobt'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
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>
…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>
0d23352 to
e89f243
Compare
… 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>
|
@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.
Your wording notes stand: e's two bounds are both 🤖 Generated with Claude Code |
OffgridwithJD
left a comment
There was a problem hiding this comment.
@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'::bitis bit(1)0, the range isBETWEEN '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_walrows match main's. - The libxml probe's
2>&1is 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)
e89f243 to
996413c
Compare
|
@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 🤖 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>
|
@OffgridwithJD, done in c1441b4, a commit on top of the chained head. #1429 has merged, so this PR's base is now on main.
#1436 sits on this PR; I'll restate it onto this head now. 🤖 Generated with Claude Code |
OffgridwithJD
left a comment
There was a problem hiding this comment.
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_typeofbounds put back, intype_matrix.shalone: 56/57, red on exactlya 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
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:"char", time, money, inet, cidr, bit(12), varbit, tsvector, xml, json (no equality), an enum, a composite, a domain over int.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:
~=, and a tsvector@@match;Evidence
cluster_testsgoes 565 → 566.🤖 Generated with Claude Code
https://claude.ai/code/session_019VvWuX1zUwk6Y513waRd4N