Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
de54465
fix(writer): ConstantEncodingEncoder now reports zone-map MIN/MAX stats
dfa1 Sep 12, 2026
5d48f24
fix(writer): RunEndEncodingEncoder now reports zone-map MIN/MAX stats
dfa1 Sep 12, 2026
471b883
fix(writer): ZigZagEncodingEncoder now reports zone-map MIN/MAX stats
dfa1 Sep 12, 2026
3f78da1
docs(changelog): consolidate the three zone-map stats entries into one
dfa1 Sep 12, 2026
a5ad964
refactor(writer): add stats-pair factories to EncodeResult/CascadeStep
dfa1 Sep 13, 2026
48003da
fix(writer): SequenceEncodingEncoder now reports zone-map MIN/MAX stats
dfa1 Sep 13, 2026
4559247
fix(writer): PcoEncodingEncoder now reports zone-map MIN/MAX stats
dfa1 Sep 13, 2026
16af880
fix(writer): RleEncodingEncoder now reports zone-map MIN/MAX stats
dfa1 Sep 13, 2026
708a58b
fix(writer): SparseEncodingEncoder now reports zone-map MIN/MAX stats
dfa1 Sep 13, 2026
c6b49da
fix(writer): PatchedEncodingEncoder now reports zone-map MIN/MAX stats
dfa1 Sep 13, 2026
e0d00a5
fix(writer): ZstdEncodingEncoder now reports zone-map MIN/MAX stats
dfa1 Sep 13, 2026
13e10c2
fix(writer): FsstEncodingEncoder now reports zone-map MIN/MAX stats
dfa1 Sep 13, 2026
1800a08
fix(writer): VarBinViewEncodingEncoder now reports zone-map MIN/MAX s…
dfa1 Sep 13, 2026
5d33db2
fix(writer): ExtEncodingEncoder#encodeCascade now reports zone-map MI…
dfa1 Sep 13, 2026
611d995
fix(writer): DateTimePartsEncodingEncoder now reports zone-map MIN/MA…
dfa1 Sep 13, 2026
1e8b7c0
test(writer): add a fitness function for zone-map stats coverage
dfa1 Sep 13, 2026
95db6d1
docs(changelog): note the ten additional zone-map stats fixes
dfa1 Sep 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `ScanIterator`'s zone-map pruning check now decodes the column's compact zone-map table (once per scan, cached) instead of fetching each chunk's own full data segment just to read its embedded stats — over HTTP, checking whether a chunk could be pruned used to cost exactly as much as fetching it outright, so pruning saved nothing for the checked column itself (only for other, unfiltered columns in the same row window). ([#380](https://github.com/dfa1/vortex-java/issues/380))
- `MaskedEncodingEncoder` (nullable columns) now computes `MIN`/`MAX` from only the row-valid elements instead of trusting whichever inner encoder ran over the dense, placeholder-filled values array — a `long[]`/`int[]`/... has no way to represent "no value", so invalid slots carry a placeholder (commonly `0`) that was silently folding into the reported stats; a column with any nulls could report a wrong non-null `MIN`/`MAX` (e.g. `0`) instead of excluding those rows, corrupting both pruning and any consumer trusting the zone-map's actual values. Found while verifying the #378 fix against the real Rust reader on the same data. ([#381](https://github.com/dfa1/vortex-java/pull/381))
- `AlpRdEncodingEncoder` (`vortex.alprd`) now computes `MIN`/`MAX` from the input `double[]`/`float[]` instead of hardcoding `null` on every path — unlike its sibling `AlpEncodingEncoder` (fixed under #379's pattern), ALP-RD never kept a running min/max alongside its dictionary training and exception encoding, so any column the cascade routed through ALP-RD lost zone-map pruning entirely regardless of how narrow or disjoint a range filter was from the data's actual domain. ([#382](https://github.com/dfa1/vortex-java/issues/382))
- `ConstantEncodingEncoder`, `RunEndEncodingEncoder`, and `ZigZagEncodingEncoder` now report `MIN`/`MAX` zone-map stats instead of hardcoding `null` — same bug as #382, three more encoders. ([#384](https://github.com/dfa1/vortex-java/issues/384), [#385](https://github.com/dfa1/vortex-java/issues/385), [#386](https://github.com/dfa1/vortex-java/issues/386))
- `SequenceEncodingEncoder`, `PcoEncodingEncoder`, `RleEncodingEncoder`, `SparseEncodingEncoder`, `PatchedEncodingEncoder`, `ZstdEncodingEncoder`, `FsstEncodingEncoder`, `VarBinViewEncodingEncoder`, `ExtEncodingEncoder` (cascade path), and `DateTimePartsEncodingEncoder` — same bug again, found by auditing every remaining encoder for the pattern rather than waiting for another report. A new `ZoneMapStatsCoverageTest` fitness function now asserts every stats-eligible encoder in the default registry actually reports stats, so a future encoder missing this can't land silently. ([#387](https://github.com/dfa1/vortex-java/pull/387))

## [0.14.1] — 2026-09-06

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,21 @@ public static CascadeStep notApplicable() {
return new CascadeStep(null, List.of(), List.of(), null, null, false);
}

/// Convenience: applicable step with open children, taking a `{min, max}`-or-`null` stats pair
/// (as returned by e.g. [PrimitiveEncodingEncoder#minMaxStats] / [VarBinEncodingEncoder#minMaxStats])
/// instead of two independently-nullable arguments.
///
/// @param partialRoot partially-assembled root encode node
/// @param ownedBuffers buffers owned directly by the root node
/// @param openChildren child slots to be filled recursively by the cascading compressor
/// @param stats a `{min, max}` pair, or `null` when neither stat is available
/// @return an applicable [CascadeStep] with `stats` unpacked into `statsMin`/`statsMax`
public static CascadeStep open(EncodeNode partialRoot, List<MemorySegment> ownedBuffers,
List<ChildSlot> openChildren, byte[][] stats) {
return new CascadeStep(partialRoot, ownedBuffers, openChildren,
stats != null ? stats[0] : null, stats != null ? stats[1] : null, true);
}

/// Returns `true` if this step has no open child slots.
///
/// @return `true` if the step is terminal (no open children)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,11 @@ public EncodeResult encode(DType dtype, Object data, EncodeContext ctx) {
}
long firstRaw = readFirstRaw(data, ptype);
ProtoScalarValue scalar = buildScalar(ptype, firstRaw);
return EncodeResult.simple(EncodingId.VORTEX_CONSTANT, MemorySegment.ofArray(scalar.encode()));
byte[] scalarBytes = scalar.encode();
// A constant array's min and max are both the one repeated value, by construction -- no
// scan needed. Empty arrays report no stats, matching every other encoder's convention.
byte[] stats = arrayLength(data, ptype) > 0 ? scalarBytes : null;
return EncodeResult.simple(EncodingId.VORTEX_CONSTANT, MemorySegment.ofArray(scalarBytes), stats, stats);
}

@Override
Expand Down Expand Up @@ -105,9 +109,8 @@ private static long readFirstRaw(Object data, PType ptype) {
};
}

private static boolean isConstant(Object data, PType ptype) {
long firstRaw = readFirstRaw(data, ptype);
int len = switch (ptype) {
private static int arrayLength(Object data, PType ptype) {
return switch (ptype) {
case I8, U8 -> ((byte[]) data).length;
case I16, U16 -> ((short[]) data).length;
case I32, U32 -> ((int[]) data).length;
Expand All @@ -116,6 +119,11 @@ private static boolean isConstant(Object data, PType ptype) {
case F64 -> ((double[]) data).length;
default -> throw new VortexException(EncodingId.VORTEX_CONSTANT, "unsupported ptype: " + ptype);
};
}

private static boolean isConstant(Object data, PType ptype) {
long firstRaw = readFirstRaw(data, ptype);
int len = arrayLength(data, ptype);
for (int i = 1; i < len; i++) {
long raw = switch (ptype) {
case I8, U8 -> ((byte[]) data)[i];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,10 @@ public EncodeResult encode(DType dtype, Object data, EncodeContext ctx) {
MemorySegment.ofArray(metaBytes),
new EncodeNode[]{daysNode, secondsNode, subsecondsNode},
new int[]{});
return new EncodeResult(root, List.copyOf(allBuffers), null, null);
// The extension's zone-map min/max is its storage primitive's, unwrapped -- here the raw
// i64 timestamp before it's split into days/seconds/subseconds, not any of the three parts
// individually (signed i64 order matches chronological order regardless of the split).
return EncodeResult.of(root, List.copyOf(allBuffers), PrimitiveEncodingEncoder.minMaxStats(PType.I64, d.timestamps()));
}

@Override
Expand Down Expand Up @@ -136,6 +139,10 @@ public CascadeStep encodeCascade(DType dtype, Object data, EncodeContext encodeC
new ChildSlot(DType.I64, seconds, 1),
new ChildSlot(DType.I64, subseconds, 2));

return new CascadeStep(partialRoot, List.of(), children, null, null, true);
// See #encode -- same open-children stats requirement as ExtEncodingEncoder#encodeCascade:
// CascadingCompressor#spliceResult takes the step's own stats verbatim, never deriving them
// from a resolved child, so the raw pre-split timestamp's stats must be computed here.
return CascadeStep.open(partialRoot, List.of(), children,
PrimitiveEncodingEncoder.minMaxStats(PType.I64, d.timestamps()));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,16 @@ public static EncodeResult simple(EncodingId encodingId, MemorySegment data) {
public boolean hasStats() {
return statsMin != null && statsMax != null;
}

/// Convenience factory taking a `{min, max}`-or-`null` stats pair (as returned by e.g.
/// [PrimitiveEncodingEncoder#minMaxStats] / [VarBinEncodingEncoder#minMaxStats]) instead of two
/// independently-nullable arguments.
///
/// @param rootNode the root encode node describing the encoding tree structure
/// @param buffers flat list of data buffers in the order referenced by `rootNode`
/// @param stats a `{min, max}` pair, or `null` when neither stat is available
/// @return an [EncodeResult] with `stats` unpacked into `statsMin`/`statsMax`
public static EncodeResult of(EncodeNode rootNode, List<MemorySegment> buffers, byte[][] stats) {
return new EncodeResult(rootNode, buffers, stats != null ? stats[0] : null, stats != null ? stats[1] : null);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,13 @@ public CascadeStep encodeCascade(DType dtype, Object data, EncodeContext ctx) {
}
EncodeNode partialRoot = new EncodeNode(EncodingId.VORTEX_EXT, null, new EncodeNode[1], new int[0]);
ChildSlot slot = new ChildSlot(ext.storageDType(), data, 0);
return new CascadeStep(partialRoot, List.of(), List.of(slot), null, null, true);
// CascadingCompressor#spliceResult takes a step's stats verbatim -- it never derives them
// from a resolved open child -- so an open storage slot needs its stats computed here,
// independently of whatever encoding the cascade eventually picks for it (matching
// ZoneMapStatCodec#zoneMinMaxDtype: an Extension's zone-map min/max is its storage
// primitive's, unwrapped).
byte[][] stats = ext.storageDType() instanceof DType.Primitive p
? PrimitiveEncodingEncoder.minMaxStats(p.ptype(), data) : null;
return CascadeStep.open(partialRoot, List.of(), List.of(slot), stats);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -79,9 +79,11 @@ public EncodeResult encode(DType dtype, Object data, EncodeContext ctx) {
new EncodeNode[]{uncompLensNode, codesOffNode},
new int[]{0, 1, 2});

return new EncodeResult(root,
List.of(c.symBuf(), c.symLenBuf(), c.compBuf(), uncompLenBuf, codesOffBuf),
null, null);
// Zone-map min/max is lexicographic-string-only, matching VarBinEncodingEncoder: a
// Binary blob isn't usefully zone-mapped.
byte[][] stats = data instanceof String[] strings ? VarBinEncodingEncoder.minMaxStats(strings) : null;
return EncodeResult.of(root,
List.of(c.symBuf(), c.symLenBuf(), c.compBuf(), uncompLenBuf, codesOffBuf), stats);
}

/// Cascading FSST: expose the per-row uncompressed-length and code-offset children as open
Expand Down Expand Up @@ -109,11 +111,12 @@ public CascadeStep encodeCascade(DType dtype, Object data, EncodeContext ctx) {
MemorySegment.ofArray(c.metaBytes()),
new EncodeNode[]{null, null},
new int[]{0, 1, 2});
return new CascadeStep(partialRoot,
byte[][] stats = data instanceof String[] strings ? VarBinEncodingEncoder.minMaxStats(strings) : null;
return CascadeStep.open(partialRoot,
List.of(c.symBuf(), c.symLenBuf(), c.compBuf()),
List.of(new ChildSlot(new DType.Primitive(c.uncompLenPType(), false), uncompLens, 0),
new ChildSlot(new DType.Primitive(c.codesOffPType(), false), codesOffsets, 1)),
null, null, true);
stats);
}

/// The FSST-specific product of compression: the symbol-table buffers, the wire code stream, the
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,14 +78,14 @@ static CascadeStep encodeCascade(DType dtype, Object data) {
DType u32Dtype = DType.U32;
DType u16Dtype = DType.U16;

return new CascadeStep(partialRoot, List.of(),
return CascadeStep.open(partialRoot, List.of(),
List.of(
new ChildSlot(dtype, fromLongs(pd.inner, ptype), 0),
new ChildSlot(u32Dtype, pd.laneOffsets, 1),
new ChildSlot(u16Dtype, pd.patchIndices, 2),
new ChildSlot(dtype, fromLongs(pd.patchValues, ptype), 3)
),
null, null, true);
PrimitiveEncodingEncoder.minMaxStats(ptype, data));
}

static EncodeResult encode(DType dtype, Object data, EncodeContext ctx) {
Expand Down Expand Up @@ -133,7 +133,8 @@ static EncodeResult encode(DType dtype, Object data, EncodeContext ctx) {
EncodeNode root = new EncodeNode(EncodingId.VORTEX_PATCHED, MemorySegment.ofArray(metaBytes),
new EncodeNode[]{innerNode, laneNode, idxNode, valNode}, new int[]{});

return new EncodeResult(root, List.of(innerBuf, laneOffsBuf, patchIdxBuf, patchValBuf), null, null);
return EncodeResult.of(root, List.of(innerBuf, laneOffsBuf, patchIdxBuf, patchValBuf),
PrimitiveEncodingEncoder.minMaxStats(ptype, data));
}

private static PatchedData computePatchedData(long[] longs, PType ptype, int n) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ static EncodeResult encode(DType dtype, Object data, EncodeContext ctx) {
int[] allBufIdxs = IntStream.range(0, buffers.size()).toArray();
MemorySegment metaBuf = buildMetadata(chunks);
EncodeNode node = new EncodeNode(EncodingId.VORTEX_PCO, metaBuf, new EncodeNode[0], allBufIdxs);
return new EncodeResult(node, buffers, null, null);
return EncodeResult.of(node, buffers, PrimitiveEncodingEncoder.minMaxStats(ptype, data));
}

private static ChunkResult encodeChunk(long[] latents, PType ptype, int dtypeSize, Arena arena) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,8 @@ public EncodeResult encode(DType dtype, Object data, EncodeContext ctx) {
MemorySegment.ofArray(metaBytes),
new EncodeNode[]{valuesNode, indicesNode, offsetsNode},
new int[0]);
return new EncodeResult(root, List.of(valuesSeg, indicesSeg, offsetsSeg), null, null);
return EncodeResult.of(root, List.of(valuesSeg, indicesSeg, offsetsSeg),
PrimitiveEncodingEncoder.minMaxStats(ptype, data));
}

private static int rleEncode(long[] input, long[] chunkValues, short[] chunkIndices) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import io.github.dfa1.vortex.core.io.VortexFormat;
import io.github.dfa1.vortex.core.io.PTypeIO;
import io.github.dfa1.vortex.core.proto.ProtoRunEndMetadata;
import io.github.dfa1.vortex.core.proto.ProtoScalarValue;

import java.lang.foreign.MemorySegment;
import java.util.ArrayList;
Expand Down Expand Up @@ -109,13 +110,24 @@ public EncodeResult encode(DType dtype, Object data, EncodeContext ctx) {
}
PType ptype = p.ptype();
int n = arrayLength(data, ptype);
boolean unsign = ptype.isUnsigned();

List<Integer> ends = new ArrayList<>();
List<Long> values = new ArrayList<>();
long minVal = 0L;
long maxVal = 0L;
if (n > 0) {
long runVal = readLong(data, ptype, 0);
minVal = runVal;
maxVal = runVal;
for (int i = 1; i < n; i++) {
long cur = readLong(data, ptype, i);
if (unsign ? Long.compareUnsigned(cur, minVal) < 0 : cur < minVal) {
minVal = cur;
}
if (unsign ? Long.compareUnsigned(cur, maxVal) > 0 : cur > maxVal) {
maxVal = cur;
}
if (cur != runVal) {
ends.add(i);
values.add(runVal);
Expand Down Expand Up @@ -149,7 +161,16 @@ public EncodeResult encode(DType dtype, Object data, EncodeContext ctx) {
EncodeNode valuesNode = EncodeNode.leaf(EncodingId.VORTEX_PRIMITIVE, 1);
EncodeNode root = new EncodeNode(EncodingId.VORTEX_RUNEND, MemorySegment.ofArray(metaBytes),
new EncodeNode[]{endsNode, valuesNode}, new int[0]);
return new EncodeResult(root, List.of(endsBuf, valuesBuf), null, null);
byte[] statsMin = n > 0 ? statsBytes(ptype, minVal) : null;
byte[] statsMax = n > 0 ? statsBytes(ptype, maxVal) : null;
return new EncodeResult(root, List.of(endsBuf, valuesBuf), statsMin, statsMax);
}

private static byte[] statsBytes(PType ptype, long value) {
if (ptype.isUnsigned()) {
return ProtoScalarValue.ofUint64Value(value).encode();
}
return ProtoScalarValue.ofInt64Value(value).encode();
}

private static int arrayLength(Object data, PType ptype) {
Expand Down
Loading
Loading