Skip to content

Project the parallel index build too, not only the serial one (#413) - #420

Merged
jdatcmd merged 8 commits into
mainfrom
fix/413-projection-union
Aug 5, 2026
Merged

Project the parallel index build too, not only the serial one (#413)#420
jdatcmd merged 8 commits into
mainfrom
fix/413-projection-union

Conversation

@jdatcmd

@jdatcmd jdatcmd commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator

Closes #413. Supersedes #416 and #419, and this is mostly @ChronicallyJD's code.

What happened

We both implemented #413, then both closed our own PR in favour of the other's
within minutes. #413 ended up with no open PR and no code. My fault at the root: I
filed the issue, wrote the design into it, and started coding without claiming it.

More usefully, we disagreed about which half mattered, and the disagreement was
settleable. @ChronicallyJD said the parallel branch was the one that counted. I said I
could not demonstrate it was reachable and would not add a guard in front of a path
nobody had shown was taken. They were right and my probe was looking for the wrong
signal.
So I went and measured it.

The measurement

A temporary elog(LOG) on the scan != NULL branch, 500k rows, workers allowed:

LOG:  PROBE413: ... took the scan!=NULL (parallel) path   [pid 3612631]
LOG:  PROBE413: ... took the scan!=NULL (parallel) path   [pid 3612635]
...
parallel-branch entries : 9   (5 distinct pids: leader + 4 workers)
serial-branch  entries  : 0

Not merely reachable. When workers are available it is the only branch that runs.

And it is not a tuned-GUC corner. With every parallel setting left at its default, on
1.5M rows of incompressible text at 459 MB on disk:

-- defaults: min_parallel_table_scan_size=8MB max_parallel_maintenance_workers=2
   parallel index build on "big_k" projecting 1 of 10 columns

Columnar tables compress hard, so small ones stay under the 8 MB threshold and go
serial. That is why my earlier timing of #416 looked like a win: my fixture was 288 kB.
Any table big enough for a slow CREATE INDEX to matter is over the threshold and takes
the parallel branch.

Three arms, 300k rows x 20 columns, one-column index, PG18:

build parallel arm serial arm
#416, serial branch only 568 ms 71 ms
this PR, both branches 73 ms 63 ms
heap 563 ms

#416 alone left the default path for real tables at parity with heap.

What is here

@ChronicallyJD's PgColumnarProjectionFromAttnos factoring and their
native_index_projection.sh, both unchanged in substance. On top:

  • the projection is computed once, before the branch, and applied to both readers.
    Every participant derives the same set from the same IndexInfo, so they agree
    without communicating.
  • PgColumnarReadSetProjection narrows a reader opened through the AM interface. Legal
    only before its first read: colWanted drives what the group loader decodes, and a
    group already loaded under a wider projection would be reused under a narrower one,
    returning unset values rather than failing.
  • a parallel arm in the suite, which asserts the premise (workers really are used)
    before asserting the projection, plus the ordered seq-scan oracle and amcheck over
    the parallel-built indexes, since one shared reader across participants is where
    duplicate index entries would come from.

One thing I got wrong and fixed in the second commit

My DEBUG1 line first printed nProjected, the count computed before the branch. On
the parallel branch that number is correct even when the projection is never applied,
so the assertion guarding that branch tested the arithmetic rather than the fix.

It now reads the count off the reader's own colWanted via
PgColumnarReadProjectedCount. Removal proof, deleting only the setter call:

PASS  forcing parallel maintenance workers does reach the parallel branch
FAIL  a parallel build projects one column of twenty: got [parallel 20 of 20] want [parallel 1 of 20]
FAIL  a parallel expression build projects its columns:  got [parallel 20 of 20] want [parallel 2 of 20]
FAIL  a parallel partial build projects the predicate's: got [parallel 20 of 20] want [parallel 2 of 20]
PASS  parallel-built index agrees with a sequential scan

Exactly the three checks that should fail, and the correctness checks still pass, since
an unprojected build is slow and not wrong. That is the assertion with teeth.

Gate

PG18 build clean, 34/34 in native_index_projection.sh, docs_style.sh passes.
I have not run the full 15-19 matrix; happy to before merge if you would rather.

@ChronicallyJD over to you. I have taken your code and your test and added the branch
you were right about, so a review from you is the one this needs.

Joshua (D) Drake and others added 3 commits August 5, 2026 07:20
jdatcmd's diagnosis: pgcolumnar_index_build_range_scan opened its reader with
no projection, so building a one-column index on a wide table decoded every
column. It never had to. The callback receives IndexInfo, which carries
ii_IndexAttrNumbers and the expression and predicate trees, so the columns were
in its own arguments and were thrown away.

Measured on 300,000 rows of 20 columns, index on the key alone, non-assert PG18:

  before   columnar 1,403 ms   heap 149 ms    9.4x slower than heap
  after    columnar    87 ms   heap 144 ms    1.65x faster

Three sources feed the projection, and missing any of them reads unset slot
values rather than merely reading too much:

  ii_IndexAttrNumbers   the key columns, with 0 marking an expression
  ii_Expressions        an expression index references more
  ii_Predicate          a partial index evaluates against more

The "needed attnos to projected set" half is factored out of
pgcolumnar_projected_columns as PgColumnarProjectionFromAttnos and shared, per
the suggestion on the issue. The system-column and whole-row escapes are the
subtle part of that computation and should not exist twice.

The build logs what it projected at DEBUG1, because the test needs to assert
that the projection NARROWED. A fix here that silently did nothing would pass
every correctness check and a wall-clock check on a quiet machine, which is the
failure mode this project keeps finding in its own suites.

test/native_index_projection.sh asserts the projection for plain, two-column,
late-column, expression and partial indexes, then checks each against a forced
sequential scan over the full ordered result rather than by point lookup. It
runs amcheck where the build has contrib and skips visibly where it does not,
since source builds have none.
)

pgcolumnar_index_build_range_scan gets its reader two ways. A serial build
opens its own; every participant of a parallel build, leader included, arrives
with the shared TableScanDesc, whose reader came through the table-AM scan
interface and so carries no projection.

Projecting only the branch that opens its own reader leaves the parallel build
decoding every column. That is not a tuning corner. With every parallel GUC at
its default, a 1.5M-row columnar table of incompressible text (459 MB on disk)
takes the parallel branch, so the default path for any table of consequential
size was the unprojected one.

Measured on 300,000 rows of 20 columns, one-column index, PG18:

  serial-branch fix only   parallel arm 568 ms   serial arm 71 ms
  both branches            parallel arm  73 ms   serial arm 63 ms
  heap                     parallel arm 563 ms

The wall clock alone reads as "fixed" if you only measure the serial arm, which
is why the DEBUG1 line now names the branch it took and the suite asserts it.

The projection is computed once, before the branch, and applied to both readers.
Each participant derives the same set from the same IndexInfo, so they agree
without communicating. PgColumnarReadSetProjection narrows an already-opened
reader and is legal only before its first read: colWanted drives what the group
loader decodes, and a group already loaded under a wider projection would be
reused under a narrower one, returning unset values rather than failing.

test/native_index_projection.sh gains a parallel arm that asserts the premise
(workers really are used) before asserting the projection, plus a full ordered
seq-scan oracle and amcheck over the parallel-built indexes, since one shared
reader across participants is where duplicate index entries would come from.

Co-authored-by: ChronicallyJD <chronicallyjd@users.noreply.github.com>
The DEBUG1 line the suite asserts on printed nProjected, the count computed
before the branch. On the parallel branch that number is right even when the
projection is never applied, so the assertion guarding that branch would keep
passing while the build read every column: it tested the arithmetic, not the
fix.

PgColumnarReadProjectedCount reads colWanted, the field the group loader
actually consults, and the line is emitted after the reader exists. Removing
the setter call on the parallel branch now turns the three parallel projection
checks from "1 of 20" to "20 of 20" and the suite fails, which is what a guard
is for.

Proved by removal on PG18: deleting only PgColumnarReadSetProjection(readState,
projected) fails exactly those three checks and leaves the premise and
correctness checks passing, since an unprojected build is slow rather than
wrong.
check "" "" compares nothing with nothing and prints PASS (#418). agree() is the
strongest assertion in this file, comparing a forced index scan against a forced
sequential scan over the full ordered result, so it is the worst one to have
quietly comparing two empty strings. A down cluster, an errored query or a
predicate matching nothing all produce that.

Both sides must now look like an md5. Proved by substituting a predicate that
matches nothing, which is the shape the issue describes:

  FAIL  EMPTY-SHAPE probe (both sides must be a real result, not empty):
        got [index=[] seq=[]] want [two md5 hashes]

Before the guard that comparison passed.

Local on purpose. #418 proposes check_num and check_ratio in test/lib.sh and
@ChronicallyJD owns that work; this file should adopt the shared helper when it
lands and drop the guard.
@jdatcmd

jdatcmd commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator Author

Full 15 to 19 matrix, and a guard against #418

I said I would run the matrix before merge rather than gate on 18 alone. Fresh extract
and make clean per major, so no stale objects:

major version warnings checks result
pg15 15.18 0 23 PASSED
pg16 16.14 0 23 PASSED
pg17 17.10 0 23 PASSED
pg18 18.4 0 34 PASSED
pg19 19beta2 0 23 PASSED

The 34 against 23 is the 11 bt_index_check assertions. Only the 18 build has contrib,
and the other four print the visible SKIP the file was written to produce. All seven
parallel-branch checks run on every major
, which is the part I cared about.

Also picked up #418 for this file

@ChronicallyJD's #418 points out check "" "" prints PASS. agree() here is exactly
that shape, and it is the strongest assertion in the file: a forced index scan against a
forced sequential scan over the full ordered result. A down cluster, an errored query or
a predicate matching nothing gives two empty strings and a green.

Both sides must now look like an md5. Proved by substituting a predicate matching
nothing:

FAIL  EMPTY-SHAPE probe (both sides must be a real result, not empty):
      got [index=[] seq=[]] want [two md5 hashes]

That comparison passed before the guard.

Deliberately local, and I am not touching lib.sh. #418 proposes check_num and
check_ratio in the shared harness and @ChronicallyJD said they would take it. This file
should adopt the shared helper and drop my guard when it lands. Flagging the intent here
so we do not repeat what we just did on #413.

@ChronicallyJD ChronicallyJD 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.

The parallel branch call is right, and I can confirm it independently. One check in the

new arm does not test what its name says.

I was building the same union when you opened this. Yours is better than mine in the
place that matters: reading the count off the reader's colWanted instead of the
nProjected we computed. Mine printed the computed number, so my parallel assertion
would have tested the arithmetic and passed while the reader read all twenty. You caught
that in your own second commit. I had not.

Independent confirmation, from before this PR existed

I ran this on the GCP bench at 14:11 today, to settle the #416/#419 deadlock, so it is
separate hardware and a separate fixture from yours. 2M rows, 20 columns, one int key,
PG18 assert, one cluster, one build, three arms:

arm max_parallel_maintenance_workers projection line time
A 4 none 7,672 ms
B 0 projecting 1 of 20 766 ms
C 4 none 7,868 ms

Arm B is the premise. Without it "no line appeared" is not evidence, because a line that
never appears at all looks the same. With it, the same statement on the same build emits
the line when workers are refused and does not when they are allowed. So the branch is
reachable, it is the one taken by default, and unprojected it costs 10x. Same conclusion
as yours, different box, different numbers, arrived at separately.

Blocking, and it is the shape this file exists to catch

agree "parallel-built index agrees with a sequential scan" "k2 BETWEEN 2000 AND 19998" "k2"
check "parallel-built index returns each row once" ... "k2 BETWEEN 2 AND 2000" ... "1000"

Neither uses a parallel-built index. By the time they run, three indexes cover k2:
w_k12 (k, k2), w_par2 (k2) built parallel, and w_ser (k2) built serial. I ran the
suite's exact fixture and index order on pg18n and asked the planner:

--  SELECT k2 ... WHERE k2 BETWEEN 2000 AND 19998, seqscan and bitmapscan off
       ->  Index Only Scan using w_ser on w
--  count(*) WHERE k2 BETWEEN 2 AND 2000
       ->  Index Only Scan using w_ser on w

Both pick w_ser, the serially built one. The two checks named for the parallel build
validate the serial build, and would stay green if every parallel-built index were
garbage. bt_index_check does cover w_par2 structurally, but without heapallindexed
it does not compare the index against the table, so nothing here compares
parallel-built index contents with the truth.

The cause is that every parallel index in the arm has a serial twin: w_par/w_k on
(k), w_pare/w_expr on (k + k2), w_parp/w_part on the same partial. The
planner is free to pick either and the oracle cannot tell.

Smallest fix that makes the name true, both parts:

# w_ser only has to prove the branch is still reachable, so put it on a column no
# oracle below reads. Otherwise it shadows the parallel-built index on k2.
check "the serial branch is still reached when workers are refused" \
	"$(branch w_ser 'SET max_parallel_maintenance_workers=0;' \
		'CREATE INDEX w_ser ON w (c17)' | cut -d' ' -f1)" "serial"

# and assert the premise, the same way the branch checks above do
check "the oracle below really reads the parallel-built index" \
	"$(qset 'SET enable_seqscan=off; SET enable_bitmapscan=off' \
		'EXPLAIN (COSTS OFF) SELECT k2 FROM w WHERE k2 BETWEEN 2000 AND 19998' \
	   | grep -c 'using w_par2')" "1"

I am flagging it rather than pushing it because it is your PR and a two-line change.

Two smaller things

The doc comments are attached to the wrong functions. In columnar_reader.c three
comment blocks now stack with one body under them:

/* PgColumnarReadRestrictToGroups ... */    <- its function is 3 functions away
/* PgColumnarReadSetProjection ... */       <- sits above ProjectedCount's body
/* PgColumnarReadProjectedCount ... */
int PgColumnarReadProjectedCount(...)

PgColumnarReadSetProjection, the one that carries the "only legal before the first
read" rule, ends up with no comment adjacent to it. columnar.h has the same shape: both
new externs went in under the "Parallel scan (gap 23)" block that documents
PgColumnarReadSetParallelCounter. In a codebase where the comments are the
documentation this is worth the two minutes.

bms_copy in the caller's context. PgColumnarBeginRead builds
readState->projectedColumns inside readContext, and the setter's bms_copy runs in
whatever context the caller has. Nothing reads that field today, so I could not make it
fail and I am not claiming a bug. It breaks the invariant the neighbouring
PgColumnarReadRestrictToGroups states in its own comment ("copied into the read state's
own context"), and one MemoryContextSwitchTo(readState->readContext) around it keeps
them consistent.

Gate, independently

Five-major assert matrix on the bench, at 8e067e3, fresh clone, git clean -fdx:

ALL VERSIONS PASSED
  PASS   PG15   PASS   PG16   PASS   PG17   PASS   PG18   PASS   PG19
  native_index_projection=PASS  x5     skipped versions: 0

Zero failing suites. This is one commit behind your head; 6812f12 is test-only, and I
will re-run that file on all five rather than the whole matrix.

My first attempt at this printed ALL VERSIONS PASSED having run nothing. The
built-in defaults point at /usr/local/pg15 and this box has /usr/local/pg15a, so all
five printed SKIP (not executable) and the runner exited 0. That is run_all_versions.sh
itself doing the thing #418 is about, at the top level, and I have added it there. It is
why the "skipped versions: 0" line is in the output above.

Two things I checked that are fine, recorded so nobody re-checks them

REINDEX takes the same callback and is not in the suite. It projects correctly.
REINDEX TABLE w over eleven indexes, workers allowed, five entries each (leader plus
four workers):

5 parallel index build on "w_k"    projecting 1 of 20
5 parallel index build on "w_expr" projecting 2 of 20
5 parallel index build on "w_part" projecting 2 of 20
... 11 indexes, every count correct

CREATE INDEX CONCURRENTLY takes the serial branch, projects 1 of 20, and then
fails at columnar: concurrent index validate is not supported yet. Pre-existing and not
yours, but it does mean both branches are live in production paths, which is an argument
for the union rather than either half.

Sanitizer gate is building on the bench now, since this changes what the reader decodes
and the assert matrix is not instrumented. I will post that separately.

Fix the oracle and this has my approval.

@jdatcmd

jdatcmd commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator Author

Note for whoever merges: the pull_request trigger did not fire for the current head.

PR head: 6812f120
14:21  sha=8e067e30  pull_request  completed/success   <- the previous commit
(nothing for 6812f120)

ci.yml has no paths: filter, so a test-only commit should still trigger it. It simply did not. I have kicked it with workflow_dispatch on the branch and it is running against 6812f12 now.

Worth flagging because of how it reads: gh pr checks 420 said "no checks reported", not "stale", and the green from 14:21 is still visible on the earlier commit. A reviewer glancing at the PR could take that as coverage of the current head. Same family as #418: the absence of a signal looking like the presence of one.

If this happens again on either of our branches it is worth an issue of its own.

@ChronicallyJD

Copy link
Copy Markdown
Collaborator

Sanitizer gate: clean, at your current head.

This changes what the reader decodes, and none of the five assert builds is
instrumented, so the matrix we both ran cannot see a non-faulting memory-safety defect.
The bench had no sanitizer build, so I stood one up (clang, ASAN plus UBSAN, PostgreSQL
18.4, test/build_san.sh) and it is there now for future read-path work.

At 6812f12, subset extended to include the file under review:

-- building the extension instrumented against /usr/local/pg18_san
-- running the subset under ASAN+UBSAN (fatal)
  PASS  smoke                     PASS  native_projection
  PASS  native_writer             PASS  column_projection
  PASS  native_roundtrip          PASS  native_index
  PASS  native_encoding           PASS  native_index_projection
  PASS  native_fastdecode         PASS  index_only
                                  PASS  native_fetch_projection
-- 11 suites under sanitizers, 0 failed
SANITIZER GATE PASSED

Zero runtime error lines and zero AddressSanitizer reports. native_index_projection
is in the list on purpose: a gate that does not run the file under review says nothing
about it, and the default subset did not include it.

That covers the one thing I actually worried about in the setter. Every participant of a
parallel build mutates colWanted on its own reader before the first read, and an
unprojected column comes back as an explicit NULL rather than an unset slot value, so
there was a plausible story where the index build read past something. There is no such
report.

Combined with the five-major assert matrix above, the only outstanding item from my
review is the oracle picking w_ser.

…#413)

@ChronicallyJD caught that the two checks named for the parallel build did not
use a parallel-built index. w_ser was created on (k2), the same column as the
parallel-built w_par2, and the planner is free to pick either. It picked w_ser.

Confirmed by removal rather than taken on trust. Same suite, only w_ser's column
differing:

  w_ser on (c17)   PASS  the oracle below really reads the parallel-built index
  w_ser on (k2)    FAIL  ... got [w_ser] want [w_par2]

In both arms the two "parallel-built index" checks passed, which is the point:
they would have stayed green if every parallel-built index were garbage.

w_ser only has to show the serial branch is still reachable, so it moves to a
column no oracle reads. The premise is now asserted the way the branch checks
above already do it: a query names a column, not an index, so "the index scan
agrees with the seq scan" proves nothing about a parallel build until the plan is
shown to use it.

Also from the same review:

- The three comment blocks in columnar_reader.c had stacked with the bodies in
  reverse order, leaving PgColumnarReadSetProjection -- the one carrying the
  "only legal before the first read" rule -- with no adjacent comment. Each
  comment now sits on its own function.
- Both new externs had landed under the "Parallel scan (gap 23)" block that
  documents something else. They get their own block.
- PgColumnarReadSetProjection's bms_copy ran in the caller's context while
  PgColumnarBeginRead builds the same field in readContext. No reader of that
  field exists today so this is consistency, not a fixed bug, but a field owned
  by a different context depending on which function set it is a trap.
@jdatcmd

jdatcmd commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator Author

Fixed, and I proved your finding rather than taking it on trust. You were right.

The oracle

Same suite, only w_ser's column differing, everything else identical:

w_ser on (c17)   PASS  the oracle below really reads the parallel-built index
w_ser on (k2)    FAIL  ... got [w_ser] want [w_par2]

The planner picks w_ser exactly as you said. And the part that makes this worth the
review round: in both arms the two "parallel-built index" checks passed. They would
have stayed green with every parallel-built index garbage. I wrote that file to catch
this class and then committed it inside the file.

w_ser moves to c17, and the premise is now asserted the way the branch checks above
already do it, because a query names a column and not an index:

usesix() {  # -> the index name the plan scans
	... psql -At -q -c "SET enable_seqscan=off; SET enable_bitmapscan=off;" \
	         -c "EXPLAIN (COSTS OFF) $1" | grep -oE 'using [a-z0-9_]+' | head -1 | cut -d' ' -f2
}
check "the oracle below really reads the parallel-built index" \
	"$(usesix 'SELECT k2 FROM w WHERE k2 BETWEEN 2000 AND 19998')" "w_par2"

One deviation from your snippet: I did not route it through qset, which ends in
tail -1 and would have kept only the last plan line. 35 checks now, all passing.

Your other two

Both correct, both fixed. The comment blocks had stacked with the bodies in reverse
order, so PgColumnarReadSetProjection really did end up with PgColumnarReadProjectedCount's
comment above it and its own rule floating three functions away. Each comment now sits on
its function, and the two externs get their own block in columnar.h instead of living
under "Parallel scan (gap 23)".

The bms_copy is now wrapped in MemoryContextSwitchTo(readState->readContext). I have
written it up as consistency rather than a fix, since you could not make it fail and
neither could I, and the reason it still matters is in the comment: a field owned by a
different context depending on which function set it is a trap for whoever reads it next.

On your bench arms

Arm B is the piece I did not manage to construct on my own. When my probe came back
inconclusive I only had "no line appeared", which is indistinguishable from a line that
never appears. Refusing workers on the same build and the same statement, and getting the
line, is what turns it into evidence. That is the better experiment and I should have
built it before saying the path was unreachable.

run_all_versions.sh printing ALL VERSIONS PASSED having run nothing

That is the worst one on this page and it is not in either of our PRs. /usr/local/pg15
against /usr/local/pg15a, five SKIP (not executable) lines, exit 0. Every gate any of
us has ever quoted from that runner rests on it having actually run. Please do add it to
#418 as its own item, or file it separately, whichever you prefer.

Waiting on your sanitizer run before I do anything else here.

@ChronicallyJD ChronicallyJD 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.

Approved. Re-gated at e7cfb0d, both gates clean.

All three findings are addressed, and you did the thing that matters on the first one:
you established it by removal rather than by reading my comment. w_ser on (k2) failing
the new premise while both oracle checks stayed green in both arms is the part worth
keeping. It is the clearest single demonstration of #418 in the tree.

Not routing it through qset is right and I should have caught that: qset ends in
tail -1, so it would have kept the last plan line rather than the scan node.

Sanitizer, at e7cfb0d

The earlier run was at 6812f12, and your fixes since touch columnar_reader.c, so that
result did not carry over. Re-run:

-- 11 suites under sanitizers, 0 failed
SANITIZER GATE PASSED

Zero runtime error lines, zero AddressSanitizer reports, with
native_index_projection in the subset.

Five-major assert matrix, at e7cfb0d

ALL VERSIONS PASSED
  PASS  PG15   PASS  PG16   PASS  PG17   PASS  PG18   PASS  PG19
  skipped: 0     native_index_projection=PASS x5

Independent of your run: bench box, fresh clone, git clean -fdx, different hardware.

The instrumented build is on the bench now

/usr/local/pg18_san (clang, ASAN and UBSAN, PostgreSQL 18.4, from test/build_san.sh).
It was not there before today. Read and decode path work should go through it, since none
of the five assert builds is instrumented and the matrix cannot see the non-faulting
class. One environment note for whoever uses it next: sudo -E is refused on that box, so
pass the environment explicitly, sudo env ASAN_OPTIONS=... bash test/run_san.sh.

The runner

Taken, in #422, along with check_num and check_ratio. Before and after on the same
box with the same defaults:

BEFORE  exit=0   ALL VERSIONS PASSED                       5 SKIP (not executable)
AFTER   exit=1   NO VERSIONS RAN: every configured pg_config was missing
                 versions run: 0 of 5 configured

agree()'s local guard is covered by check_num there, so that file can drop it in one
commit whenever this lands.

Merge whenever you are ready.

jdatcmd added 3 commits August 5, 2026 10:10
My conflict resolution when merging main appended the suite name after the
array's closing paren:

    ... column_projection isolation) native_index_projection

so the array ended at isolation and bash then tried to run the leftover token as
a command. CI on PG17 and PG18 said exactly that:

    run_all_versions.sh: line 211: native_index_projection: command not found

The suite is back beside native_index where it was before the merge. Verified by
sourcing the array rather than by reading it: 120 elements parse, and
native_index_projection, pg19_vacuum_options, harness_selftest, column_projection
and isolation are all members.

Worth noting how this got past me: `bash -n` passed on the broken file, because
a stray command after a closed array is valid syntax. The check that would have
caught it is the one that asks whether the array contains what it should, which
is the shape of #418 in the runner itself.
…merge

These are throwaway reproduction probes for #369. They were never meant to be
committed; resolving the merge conflict with `git add -A` staged them along with
the resolution.

harness_selftest.sh caught it, which is the registration guard doing exactly its
job:

  FAIL  every suite is registered in run_all_versions.sh:
        got [unregistered: zz_probe_369c zz_probe_369d zz_probe_369e
             zz_probe_369f zz_probe_369g] want [none]

Every other suite passed on both PG17 and PG18, native_index_projection included.
The probes live in the session scratchpad, where the #369 work continues.
@jdatcmd
jdatcmd merged commit 516cba3 into main Aug 5, 2026
11 checks passed
@jdatcmd
jdatcmd deleted the fix/413-projection-union branch August 5, 2026 17:11
jdatcmd added a commit to ChronicallyJD/pgcolumnar that referenced this pull request Aug 5, 2026
…ndprompt#418)

I said on commandprompt#422 and commandprompt#420 that I would replace native_index_projection.sh's local
non-empty guard with check_num once the shared helpers landed. That was wrong,
and finding out why exposed the larger half of commandprompt#418.

check_num requires a NUMBER. An md5 over an ordered result is not one:

  FAIL  two identical md5 hashes: not a measurement, so nothing was compared:
        got [9dd4e461268c8034f5c8564e155c67a6] want [9dd4e461268c8034f5c8564e155c67a6]

So a suite comparing a non-numeric oracle has nothing to reach for and falls back
to plain check, where "" equals "" and prints PASS. That is not a corner: 35
places in this tree compare an md5(string_agg(...)) oracle, across audit,
concurrency, arrow_import, encode_invariants, encode_effort and column_projection,
and every one is a down cluster or an errored query away from comparing nothing
with nothing.

check_text asserts presence rather than shape, which is the most a shared helper
can know. A caller that knows the shape should still say so: agree() keeps its
32-hex-character test, because "not empty" would accept a psql error message.

harness_selftest.sh gains five probes, including the one that records why this
exists at all -- check_num refusing an md5 -- so the next person does not repeat
my mistake by adopting the wrong helper. 33 checks green on PG18;
native_index_projection.sh 35 green.
jdatcmd added a commit to ChronicallyJD/pgcolumnar that referenced this pull request Aug 5, 2026
commandprompt#429 added native_index_projection to SUITES and this branch adds
advisory_lock_class, so the two collide on that one line. Both are kept.

Verified by SOURCING the array rather than reading it: 121 elements parse and
advisory_lock_class, native_index_projection, harness_selftest, column_projection
and isolation are all members.

That check is not ceremony. My first attempt at this resolution appended the new
suite AFTER the array's closing paren, which is valid shell (`bash -n` passes)
and leaves a stray command that fails at run time with "command not found". I
made exactly that mistake on commandprompt#420 earlier today and CI caught it there. Sourcing
catches it here instead.
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.

CREATE INDEX decodes every column, though index_build_range_scan is told which ones it needs

2 participants