Summary
AlpRdEncodingEncoder (vortex.alprd, writer module) never computes or reports zone-map min/max stats — both its result-building paths hardcode null, null regardless of the input data or cascading depth. Any column the cascade selects ALP-RD for loses zone-map pruning entirely: RowFilter-driven pruning (ScanIterator#canPruneChunk) can never skip a chunk with no stats to prune with, so a filter fetches the whole column no matter how narrow the range or how disjoint from the data's actual domain.
This is a different root cause from #379 (cascade-path dropping stats that the terminal path did compute): ALP-RD's own encode() never computes stats at all, in any path.
Where
writer/src/main/java/io/github/dfa1/vortex/writer/encode/AlpRdEncodingEncoder.java:
buildEncodeResult(...) — line 299: return new EncodeResult(root, List.copyOf(allBuffers), null, null);
emptyResult(...) — line 325: same, null, null
Compare the sibling (non-RD) AlpEncodingEncoder, which does compute real stats on every path (writer/src/main/java/io/github/dfa1/vortex/writer/encode/AlpEncodingEncoder.java:192-193):
byte[] statsMin = n > 0 ? scalarF64(min) : null;
byte[] statsMax = n > 0 ? scalarF64(max) : null;
ALP-RD tracks left/right bit-split codes and exception patches but never keeps a running min/max over the input double[]/float[] while doing so.
Reproduction
File: timestamp:i64:series(1700000000000,1000), price:f64:range(50,150), 200,000,000 rows, WriteOptions.cascading(3) (the demo's default). vortex-cli inspect confirms price alone is missing min=/max= in its summary line, while timestamp/volume/symbol all show one:
timestamp: vortex.stats(...) → ... min=1700000000000 max=1899999999000
symbol: vortex.stats(...) → ... min=0 max=29
price: vortex.stats(...) → ... <-- no min=/max= at all
volume: vortex.stats(...) → ... min=100 max=10000
Consequence: a price range filter completely outside the column's actual domain (e.g. [5, 6], when every value is in [50, 150)) still fetches ~54% of the file (1.3 GB of a 2.3 GB file) instead of pruning every chunk. A filter that does overlap the domain (e.g. [50, 60]) fetches the same ~54% regardless of how narrow the overlap is — every chunk is decoded in full either way.
Impact
Silent, not a crash. Any file with a float/double column that the cascade routes through ALP-RD loses pruning on that column entirely, on top of paying the bandwidth/decode cost of every chunk regardless of filter selectivity. AlpEncodingEncoder shows the fix shape already exists in the codebase; ALP-RD just needs the same running min/max kept alongside its dictionary training and exception encoding.
How this was found
Found live-testing vortex-demo (#377) against a real 200M-row/2.4 GB file: after fixing #378/#379/#380 and a correctness bug in the demo's own row-counting, a price filter still fetched roughly the same ~54% of the file no matter how narrow or how completely outside the data's range the filter was — pointing at missing stats rather than the "no natural clustering" explanation that fit the symbol/enum case.
🤖 Filed via Claude Code
https://claude.ai/code/session_01P4ijFsGW1MHEcGiu26vNzi
Summary
AlpRdEncodingEncoder(vortex.alprd, writer module) never computes or reports zone-mapmin/maxstats — both its result-building paths hardcodenull, nullregardless of the input data or cascading depth. Any column the cascade selects ALP-RD for loses zone-map pruning entirely:RowFilter-driven pruning (ScanIterator#canPruneChunk) can never skip a chunk with no stats to prune with, so a filter fetches the whole column no matter how narrow the range or how disjoint from the data's actual domain.This is a different root cause from #379 (cascade-path dropping stats that the terminal path did compute): ALP-RD's own
encode()never computes stats at all, in any path.Where
writer/src/main/java/io/github/dfa1/vortex/writer/encode/AlpRdEncodingEncoder.java:buildEncodeResult(...)— line 299:return new EncodeResult(root, List.copyOf(allBuffers), null, null);emptyResult(...)— line 325: same,null, nullCompare the sibling (non-RD)
AlpEncodingEncoder, which does compute real stats on every path (writer/src/main/java/io/github/dfa1/vortex/writer/encode/AlpEncodingEncoder.java:192-193):ALP-RD tracks left/right bit-split codes and exception patches but never keeps a running min/max over the input
double[]/float[]while doing so.Reproduction
File:
timestamp:i64:series(1700000000000,1000),price:f64:range(50,150), 200,000,000 rows,WriteOptions.cascading(3)(the demo's default).vortex-cli inspectconfirmspricealone is missingmin=/max=in its summary line, whiletimestamp/volume/symbolall show one:Consequence: a
pricerange filter completely outside the column's actual domain (e.g.[5, 6], when every value is in[50, 150)) still fetches ~54% of the file (1.3 GB of a 2.3 GB file) instead of pruning every chunk. A filter that does overlap the domain (e.g.[50, 60]) fetches the same ~54% regardless of how narrow the overlap is — every chunk is decoded in full either way.Impact
Silent, not a crash. Any file with a
float/doublecolumn that the cascade routes through ALP-RD loses pruning on that column entirely, on top of paying the bandwidth/decode cost of every chunk regardless of filter selectivity.AlpEncodingEncodershows the fix shape already exists in the codebase; ALP-RD just needs the same running min/max kept alongside its dictionary training and exception encoding.How this was found
Found live-testing
vortex-demo(#377) against a real 200M-row/2.4 GB file: after fixing #378/#379/#380 and a correctness bug in the demo's own row-counting, apricefilter still fetched roughly the same ~54% of the file no matter how narrow or how completely outside the data's range the filter was — pointing at missing stats rather than the "no natural clustering" explanation that fit thesymbol/enum case.🤖 Filed via Claude Code
https://claude.ai/code/session_01P4ijFsGW1MHEcGiu26vNzi