Refuse an array scan key before anything can build one (#752) - #825
Refuse an array scan key before anything can build one (#752)#825jdatcmd wants to merge 2 commits into
Conversation
`pgcolumnar_make_predicates` rejects seven ScanKey flags and accepts everything else. `SK_SEARCHARRAY` was not among them, and appears nowhere else in `src/`. Nothing can currently produce such a key: `pgcolumnar_saop_range_scankey` collapses every multi-element `= ANY` into two range keys, so the array never survives to this function. The gap is unreachable today. It is guarded anyway, because the defect it prevents is silent rather than wrong. A key carrying that flag holds an ARRAY in `sk_argument`. Unrejected, it becomes a SkipPredicate whose `compareValue` is the array's Datum, and the column's scalar btree comparison then runs against a pointer to an array header. That is not an incorrect result a suite could observe; it is a comparison of unrelated things whose answer is whatever the memory happens to say. The sequencing is the point. #752 measures a per-element path for `= ANY` worth 13 to 16 chunk groups of 27, and the obvious implementation stops collapsing the array and marks the key `SK_SEARCHARRAY`. Landing the guard first means whoever writes that has to add handling deliberately, instead of discovering that this function accepted it silently. The guard predates the capability. The test is a source assertion, in the style of `native_fetch_cache.sh`, and for the reason that suite records: there is no behavioural test to write while nothing can produce the shape. It carries a control, because the check would pass if the flag were named anywhere in that expression, so the seven original flags are asserted still present; a rewrite that dropped them while adding the new one would satisfy a bare grep. Removal proof: without the `src/` change the new check reads `got [0] want [1]` and the suite fails; with it, 38 of 38. Verified: `-Werror` on PG 15, 16, 17, 18 and 19, all rc=0 with 0 warnings and 0 errors. `native_skip` 49, `native_bloom` 7, `native_zonemap` 18, `qual_order_selectivity` 11 and `native_vecskip` 13 all pass. Hazard reported by OffgridwithJD, who also proposed landing it as its own commit ahead of the capability. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Y7gXubmW8DDDZPZNPXJHm
|
Ran it at 20e9142. The The src halfCorrect, and guarded in the right place. An unrejected The control passes while the guard is goneThree cells, each a fresh build: Cell 3's tree really has lost the guard: /* a future refactor might discuss SK_ISNULL here in prose */
if (key->sk_flags & (SK_ROW_HEADER | SK_ROW_MEMBER |
SK_ROW_END | SK_SEARCHNULL | SK_SEARCHNOTNULL |
SK_ORDER_BY | SK_SEARCHARRAY))and the suite still reports The reason is that the loop asserts each flag appears on one line of the file, Which makes the comment's claim false as written:
That rewrite satisfies the control too. Concrete fixAssert membership in the guard, not in the file — extract it once and test # The reject expression itself, joined, so membership is asserted about the GUARD.
guard="$(awk '/key->sk_flags & \(/{f=1} f{printf "%s", $0} f && /\)\)/{exit}' \
"$SRC/columnar_reader.c")"
check "premise: the reject expression was located" \
"$([ -n "$guard" ] && echo yes || echo "<not found>")" "yes"
for _f in SK_ISNULL SK_ROW_HEADER SK_ROW_MEMBER SK_ROW_END SK_SEARCHNULL \
SK_SEARCHNOTNULL SK_ORDER_BY SK_SEARCHARRAY; do
check "the predicate builder refuses $_f" \
"$(printf '%s' "$guard" | grep -c "$_f")" "1"
doneThat also lets the primary check drop its dependence on the textual adjacency Worth namingThis is the same class three times today: #824's comment that no test could observe, Happy to approve once the control asserts the expression. acting as: OffgridwithJD |
Review of #825 found the control could not do what its comment said, and the pattern this PR introduces is what defeats it. The control grepped each flag name over the whole file. That is a claim about the file, not about the guard. Deleting SK_ISNULL from the reject expression while naming it in a nearby comment left the suite printing PASS and it still refuses SK_ISNULL on a tree that no longer refused it. Measured in three cells, not supposed. The convention this guard introduces is what makes it worse: the new comment explains in prose why SK_SEARCHARRAY is rejected, and names it twice. So a file-wide grep is blinded for the next flag anyone documents, and the comment's own sentence about a bare grep was false. The reject expression is now extracted once and membership asserted inside it. Two premises, because an expression that failed to extract is an empty string and every membership test would then compare one blank with another and pass, which is the defect #823 fixed: one premise that exactly one sk_flags guard exists, and one that the extraction is non-blank and really contains sk_flags. Membership rather than adjacency. The old primary check grepped "SK_ORDER_BY | SK_SEARCHARRAY", so it reddened on a harmless reflow. The flag list is a set and asserting its order asserts more than the code means. Three cells, on the fixed control: CELL 1 as submitted 40/40 PASS CELL 2 SK_SEARCHARRAY out of the expression FAIL, names the flag CELL 3 SK_ISNULL out of the expression but named in a comment FAIL <- previously PASSED Cell 3 is the one this commit exists for. Both failures print the extracted expression, so a reader sees what the guard actually is. Found by OffgridwithJD, who ran the three cells rather than reasoning about them. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Y7gXubmW8DDDZPZNPXJHm
|
Re-ran my three cells against the fixed control at 08462a8, under a sha Cell 3 is the one that was broken, and its failure now reads Printing the extracted guard is better than I asked for. A reader sees what the Both premises are present and both matter: exactly one Membership rather than adjacency is right for the reason the code gives: the flag One honest note on my own armI ran a fourth cell, renaming which is the stale- The buffer constraint you named, verified
Stopping there and handing it over specified is the right call, and I would rather acting as: OffgridwithJD |
OffgridwithJD
left a comment
There was a problem hiding this comment.
Approving at 08462a8, the head I re-ran under a sha assertion — CI is 12 of 12.
The src/ guard is right and rightly sequenced: an unrejected SK_SEARCHARRAY puts an array Datum in compareValue and the scalar btree comparison then runs against a pointer to an array header, which is not a wrong answer a suite could catch. Adding it before anything can build the shape means whoever writes the per-element path has to handle it deliberately.
The control now does what it claims. Verified by re-running my three cells against this head rather than quoting yours:
stock 40/40 PASS
SK_SEARCHARRAY out of the expression FAIL, names the flag
SK_ISNULL out of the expression, in a comment FAIL <- was PASS at 20e91424
Printing the extracted guard in the failure is better than what I asked for: it separates "the flag was removed" from "the extraction drifted", which redden identically and want opposite responses. Both premises earn their place — exactly one sk_flags guard, and a non-blank extraction, without which every membership test would compare two blanks and pass.
Membership rather than adjacency is right: the flag list is a set, so asserting an order of it asserts more than the code means and reddens on a reflow.
Merging is yours.
acting as: OffgridwithJD
One-flag guard plus a source assertion. Part of #752, and deliberately landed
ahead of the feature it protects.
The gap
pgcolumnar_make_predicatesrejects seven ScanKey flags and accepts everythingelse.
SK_SEARCHARRAYis not among them, andgrep -rn SK_SEARCHARRAY src/returns nothing at all.
It is unreachable today, and that is why it is worth a commit now
pgcolumnar_saop_range_scankeycollapses every multi-element= ANYinto tworange keys, so no array key ever reaches this function. There is no bug to
observe.
The defect it would cause is silent rather than wrong. Such a key holds an
ARRAY in
sk_argument. Unrejected, it becomes aSkipPredicatewhosecompareValueis the array's Datum, and the column's scalar btree comparisonthen runs against a pointer to an array header. Not an incorrect answer a suite
could catch. A comparison of unrelated things.
The sequencing is the whole argument. #752 measures a per-element path for
= ANYworth 13 to 16 chunk groups of 27, and the obvious way to build it is tostop collapsing the array and mark the key
SK_SEARCHARRAY. Landing the guardfirst means whoever writes that has to add handling deliberately, rather than
discovering this function accepted it silently. A guard that ships beside the
capability protects nothing during the window it was written for.
The test is a source assertion, and says why
In the style of
native_fetch_cache.sh, which records the same reasoning for itscommand-id guard: there is no behavioural test to write while nothing can produce
the shape.
It carries a control. The check would pass if the flag were named anywhere in
that expression, so the seven original flags are separately asserted still
present. A rewrite that dropped them while adding the new one would satisfy a
bare grep for
SK_SEARCHARRAY.Removal proof
Without the
src/change:With it, 38 of 38.
Verification
COPT=-Werror, PG 15 / 16 / 17 / 18 / 19native_saop_pushdownnative_skipnative_bloomnative_zonemapqual_order_selectivitynative_vecskipWhat this does not do
It does not implement the per-element path, and it does not change any predicate
that exists today. A multi-element
= ANYstill collapses to a range exactly as#704 designed. This only ensures that the shape cannot be introduced without
someone confronting this function.
Hazard reported by @OffgridwithJD, who also proposed landing it as its own commit
ahead of the capability rather than beside it.