Skip to content

The batch fold ignores the skip vector, so making decode honour it would answer from uninitialised memory #512

Description

@ChronicallyJD

Two facts that are harmless today and become a silent wrong answer together, the
moment anyone makes decode honour the skip vector — which is the obvious next
optimisation and is Phase 1b of #452.

Reported by jdatcmd from reading; verified here before filing.

1. The batch fold fetches the skip vector and never reads it

columnar_vector.c:3161 declares const bool *skipVec, :3167 fills it from
PgColumnarReadFoldGroupInfo, and skipVec[ appears nowhere in the file.

It is correct today for a reason that has nothing to do with the skip vector: the
fold evaluates the scan keys itself, per value, at :3234
(pgcolumnar_batch_key_pass over every key for every row). So it reads vectors
the zone maps already ruled out and re-checks each value by hand. Wasteful, and
right.

2. Decode never skips anything, and "Vectors Skipped" does not mean it does

pgcolumnar_native_decode_chunk (columnar_reader.c:768) takes no skip mask:

static char *
pgcolumnar_native_decode_chunk(MemoryContext cx, Form_pg_attribute att,
                             char *values, uint32 valuesLen,
                             const char *desc, uint32 descLen, int blockCodec,
                             uint32 **outVecRawLen, int *outVecCount)

A chunk is decoded whole at group load. vectorsSkipped — the counter behind
EXPLAIN's Columnar Vectors Skipped — is incremented at columnar_reader.c:1691,
in the branch that advances the value cursors past a vector. So the line means
"not turned into Datums", never "not decoded". #452's text says only that
decompression is not skipped, which understates it: nothing about decode is
skipped, and the EXPLAIN line invites the opposite reading.

Why they are one issue

Skipping a vector's decode leaves a hole in the raw buffer. The row producer
survives that — it steps its cursors past skipped vectors and never dereferences
the gap. The fold does not: it walks every value in the group and re-checks it
against the scan keys, so it would read uninitialised memory and answer from it.
An aggregate returning a wrong number, with no error and no crash.

And it is the common case, not an edge. pgcolumnar_batch_shape_eligible requires
every qual to be convertible to a scan key, so the fold runs precisely when
predicates exist
— which is exactly when vectors get skipped.

What I would do

Not "make the fold read skipVec" as a follow-up to 1b. The ordering is the point:
the fold must honour the skip vector before decode is allowed to skip
anything
, and whoever writes the decode change is the person least likely to
look in columnar_vector.c.

Cheapest safe interim, if 1b is not imminent: have the fold assert that no vector
in the group is skipped, so the day someone changes decode they get an assertion
rather than an arithmetic error. That is a few lines and it converts a silent
wrong answer into a loud one.

Separately, Columnar Vectors Skipped should either be renamed to say what it
counts, or gain a sibling once decode can skip, so the two are not conflated in a
plan someone is using to decide whether decode is the bottleneck.

Not measured

This is a reading of the code and a hazard analysis, not a reproduction — the
decode change does not exist, so there is nothing to reproduce yet. Filing it now
rather than after, because the trigger is a change someone is about to make and
the failure it produces is silent.

Refs #452

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions