Skip to content

Zone-map min/max stats lost for any column encoded via the cascade (allowedCascading > 0) #379

Description

@dfa1

Summary

With WriteOptions.cascading(depth) for any depth > 0, every numeric column I tested loses its zone-map min/max statistics entirely — only sum survives. This silently breaks RowFilter-driven zone-map pruning (ScanIterator#canPruneChunk) for any column written through the cascade, regardless of type (i64, f64 both affected) and regardless of whether the data has any special structure (a purely arithmetic series and uniformly random doubles both lose stats the same way). At WriteOptions.cascading(0) (equivalent to WriteOptions.defaults()), the same columns get correct min/max.

This is a broader, more consequential version of #378 (which was specifically about globalDict + Utf8): that one only affected dict-encoded string columns; this one appears to affect any column that goes through a cascade competition with allowedCascading > 0 at all.

Reproduction

Two-column file (timestamp:i64:series(1700000000000,1000), price:f64:range(50,150)), 200,000 rows:

cascading depth timestamp zone 0 stats price zone 0 stats
0 min=1700000000000, max=1700065535000, sum=111413347450880000 min=50.0006..., max=149.9985..., sum=6553292.68
3 min=null, max=null, sum=111413347450880000 min=null, max=null, sum=6546631.49

(sum values differ slightly between rows because the two runs used different row counts, not because of the cascading depth — the point is min/max disappear entirely at depth 3 while sum is always present.)

Consequence measured on a 2,000,000-row file: a RowFilter.gte("timestamp", ...).and(RowFilter.lte("timestamp", ...)) range covering rows [1,000,000, 1,050,000) (2.5% of the file) — a query that should prune all but a couple of chunks — instead yields all 31 of 31 chunks, matching all 2,000,000 rows.

Where I looked

CascadingCompressor#spliceResult (writer module) does propagate step.statsMin()/step.statsMax() from the winning CascadeStep into the final EncodeResult — the propagation plumbing itself looks correct. EncodingEncoder's default encodeCascade() (used by encoders that don't override it) wraps a plain encode() result, including its stats, so those encoders should be fine either way.

My working hypothesis, not fully confirmed: encoders that do override encodeCascade() to expose intermediate/child representations for cascading (ALP, FrameOfReference, BitPacked, Delta, etc.) return a CascadeStep whose statsMin()/statsMax() are left null, even though their own plain terminal encode() path computes proper min/max. Since depth 0 only permits terminal encodings (per CascadingCompressor's own doc comment), it always picks a stats-preserving path; depth > 0 lets a cascade-capable encoder win, and that path apparently only bothers wiring up sum (presumably for the ADR 0013 SUM-pushdown reduction), not min/max.

I have not traced this all the way to a single line/encoder — it may be one shared gap in a common helper, or a per-encoder omission repeated across several encodeCascade() overrides. Worth an actual audit of each cascade-capable encoder's encodeCascade() before fixing, or centralizing min/max computation in CascadingCompressor itself so it doesn't depend on each encoder remembering to do it.

Impact

Silent, not a crash — the scan still returns correct results, just without any pruning speedup, on top of the cost of decoding much more data than necessary. Given cascading() is a common choice for real compression benefit (e.g. JdbcImportOptions.defaults() already uses WriteOptions.cascading(3)), this likely affects a broad swath of realistic writes, not an edge case.

How this was found

Found while building a demo (vortex-server/vortex-demo/vortex-fakedata-generator, see #377) showing VortexHttpReader's partial-fetch story over HTTP. After working around #378 (globalDict), a numeric time-range filter on a plain i64 series column still fetched the entire file — this issue is why. The demo currently works around it by generating with --cascading 0.

🤖 Filed via Claude Code

https://claude.ai/code/session_01P4ijFsGW1MHEcGiu26vNzi

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions