Skip to content

Commit 0eea040

Browse files
dfa1claude
andcommitted
feat(reader): lazy VarBinView decode via VarBinArray.ViewMode
Add ViewMode as a fourth sealed implementation of VarBinArray, alongside OffsetMode, DictMode, and ChunkedMode. ViewMode holds the 16-byte-per-row views buffer plus zero or more shared data buffers and resolves accessors on demand: getBytes(i): inline-or-referenced view, byte[]-copy from views or dataBufs getString(i): UTF-8 over getBytes getByteLength(i): u32 size from the view header (no data read) forEachByteLength: iterate view sizes only (no string copies) truncate(rows): views.asSlice(0, rows*16); dataBufs unchanged VarBinViewEncodingDecoder.decode now returns the lazy ViewMode directly instead of walking views, allocating outBytes + outOffsets, and copying every row into the flat OffsetMode shape. Three other decoders treat the values child as VarBinArray.OffsetMode and casted directly — DictEncodingDecoder.decodeUtf8DictProto for the dict-values table, SparseEncodingDecoder.decodeVarBin for the per-patch values list, and RunEndEncodingDecoder for Utf8/Binary run values. Add a centralised VarBinArray.toOffsetMode(VarBinArray, SegmentAllocator) that returns the input unchanged when it is already OffsetMode and materialises any other shape (ViewMode) into a flat OffsetMode on the fly. The cost is identical to the previous eager-VarBinView path; it just shifts from "always materialise at scan output" to "materialise only when a parent decoder demands the bytes-plus-offsets shape." Same shape can pick up ChunkedMode / DictMode if those ever land as a child. ScanIterator's expandDictStrings fallback path uses the same helper. 6 new unit tests in VarBinViewModeTest cover inline accessor, referenced accessor, mixed inline+referenced, forEachByteLength, truncate prefix, and the truncate-beyond-length identity. ./mvnw verify green (13 modules, integration suite 45s). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent a2ea479 commit 0eea040

7 files changed

Lines changed: 277 additions & 49 deletions

File tree

reader/src/main/java/io/github/dfa1/vortex/reader/ScanIterator.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -603,7 +603,8 @@ private Array decodeDictLayout(Layout dictLayout, DType dtype, SegmentAllocator
603603
throw new VortexException(EncodingId.VORTEX_DICT,
604604
"dict codes: layout row_count=" + n + " exceeds buffer capacity=" + bufferCodesFallback);
605605
}
606-
return expandDictStrings((VarBinArray.OffsetMode) values, codesSegFallback, codesPType, dtype, n, arena);
606+
return expandDictStrings(VarBinArray.toOffsetMode((VarBinArray) values, arena),
607+
codesSegFallback, codesPType, dtype, n, arena);
607608
}
608609

609610
/// Lazy-path zip-bomb guard. Inspects {@code codes}'s primary segment when available

reader/src/main/java/io/github/dfa1/vortex/reader/array/VarBinArray.java

Lines changed: 111 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,21 @@
66
import io.github.dfa1.vortex.encoding.PTypeIO;
77

88
import java.lang.foreign.MemorySegment;
9+
import java.lang.foreign.SegmentAllocator;
910
import java.lang.foreign.ValueLayout;
1011
import java.nio.charset.StandardCharsets;
1112
import java.util.function.IntConsumer;
1213

1314
/// Sealed interface for variable-length binary / UTF-8 string columns.
1415
///
15-
/// Three implementations: {@link OffsetMode} for standard offset-based layout,
16-
/// {@link DictMode} for dictionary-encoded strings, and {@link ChunkedMode} for
17-
/// multi-chunk columns. All accessors resolve transparently regardless of mode;
18-
/// only {@link OffsetMode} exposes {@link OffsetMode#offsetsSegment()} and
19-
/// {@link OffsetMode#offsetsPtype()}.
16+
/// Four implementations: {@link OffsetMode} for standard offset-based layout,
17+
/// {@link DictMode} for dictionary-encoded strings, {@link ChunkedMode} for
18+
/// multi-chunk columns, and {@link ViewMode} for Arrow StringView / BinaryView
19+
/// layout (16-byte view per row + zero or more shared data buffers). All
20+
/// accessors resolve transparently regardless of mode; only {@link OffsetMode}
21+
/// exposes {@link OffsetMode#offsetsSegment()} and {@link OffsetMode#offsetsPtype()}.
2022
public sealed interface VarBinArray extends Array
21-
permits VarBinArray.OffsetMode, VarBinArray.DictMode, VarBinArray.ChunkedMode {
23+
permits VarBinArray.OffsetMode, VarBinArray.DictMode, VarBinArray.ChunkedMode, VarBinArray.ViewMode {
2224

2325
/// Returns the concatenated raw bytes segment backing all elements.
2426
///
@@ -54,6 +56,38 @@ public sealed interface VarBinArray extends Array
5456
/// @return a {@code VarBinArray} containing the first {@code rows} elements
5557
VarBinArray truncate(long rows);
5658

59+
/// Materialises any {@code VarBinArray} into a flat {@link OffsetMode}. The fast path
60+
/// returns {@code src} unchanged when it is already an {@link OffsetMode}. Other modes
61+
/// (ViewMode in particular) walk every row through the typed accessors, copy the bytes
62+
/// into a fresh contiguous segment allocated from {@code arena}, and build an I64
63+
/// offsets table. Used by parent decoders (dict, sparse, runend) whose downstream code
64+
/// depends on the bytes-plus-offsets shape.
65+
///
66+
/// @param src any VarBinArray
67+
/// @param arena allocator for the materialised bytes and offsets segments
68+
/// @return an OffsetMode view over the same logical content
69+
static OffsetMode toOffsetMode(VarBinArray src, SegmentAllocator arena) {
70+
if (src instanceof OffsetMode om) {
71+
return om;
72+
}
73+
long n = src.length();
74+
long totalBytes = 0;
75+
for (long i = 0; i < n; i++) {
76+
totalBytes += src.getByteLength(i);
77+
}
78+
MemorySegment outBytes = arena.allocate(totalBytes > 0 ? totalBytes : 1);
79+
MemorySegment outOffsets = arena.allocate((n + 1) * Long.BYTES, Long.BYTES);
80+
outOffsets.setAtIndex(PTypeIO.LE_LONG, 0, 0L);
81+
long bytePos = 0;
82+
for (long i = 0; i < n; i++) {
83+
byte[] b = src.getBytes(i);
84+
MemorySegment.copy(MemorySegment.ofArray(b), 0, outBytes, bytePos, b.length);
85+
bytePos += b.length;
86+
outOffsets.setAtIndex(PTypeIO.LE_LONG, i + 1, bytePos);
87+
}
88+
return new OffsetMode(src.dtype(), n, outBytes.asReadOnly(), outOffsets, PType.I64);
89+
}
90+
5791
/// Creates a dict-mode {@code VarBinArray}. Lengths and bytes are resolved via the
5892
/// dictionary on each access; no string materialization occurs at construction time.
5993
///
@@ -332,4 +366,75 @@ public VarBinArray truncate(long rows) {
332366
return ChunkedMode.of(dtype, rows, kept);
333367
}
334368
}
369+
370+
/// Arrow StringView / BinaryView {@code VarBinArray}.
371+
///
372+
/// Each row is a 16-byte view in {@code views}: bytes 0-3 are the u32 size; for
373+
/// sizes ≤ 12 bytes the data is inlined in bytes 4..15; for sizes > 12 bytes
374+
/// bytes 4-7 hold a 4-byte prefix (ignored on read), bytes 8-11 the u32 buffer
375+
/// index into {@code dataBufs}, and bytes 12-15 the u32 offset within that
376+
/// buffer. Per-row accessors resolve the view on demand — no concat or
377+
/// materialisation at construction time.
378+
///
379+
/// {@link #bytesSegment()} returns {@link MemorySegment#NULL} because there is
380+
/// no single contiguous bytes segment; callers needing one must materialise via
381+
/// the typed accessors.
382+
///
383+
/// @param dtype logical element type (Utf8 or Binary)
384+
/// @param length total logical row count
385+
/// @param views 16-byte view per row; length must be ≥ {@code length * 16}
386+
/// @param dataBufs zero or more shared data buffers referenced by long views
387+
@SuppressWarnings("java:S6218") // internal data carrier; record components are arrays of immutable refs that flow through pipelines without ever being compared.
388+
record ViewMode(DType dtype, long length, MemorySegment views, MemorySegment[] dataBufs)
389+
implements VarBinArray {
390+
391+
private static final int VIEW_SIZE = 16;
392+
private static final int MAX_INLINED_SIZE = 12;
393+
394+
@Override
395+
public MemorySegment bytesSegment() {
396+
return MemorySegment.NULL;
397+
}
398+
399+
@Override
400+
public int getByteLength(long i) {
401+
return views.get(PTypeIO.LE_INT, i * VIEW_SIZE);
402+
}
403+
404+
@Override
405+
public byte[] getBytes(long i) {
406+
long viewOff = i * VIEW_SIZE;
407+
int size = views.get(PTypeIO.LE_INT, viewOff);
408+
byte[] out = new byte[size];
409+
if (size <= MAX_INLINED_SIZE) {
410+
MemorySegment.copy(views, viewOff + 4, MemorySegment.ofArray(out), 0, size);
411+
} else {
412+
int bufferIndex = views.get(PTypeIO.LE_INT, viewOff + 8);
413+
long srcOffset = Integer.toUnsignedLong(views.get(PTypeIO.LE_INT, viewOff + 12));
414+
MemorySegment.copy(dataBufs[bufferIndex], srcOffset, MemorySegment.ofArray(out), 0, size);
415+
}
416+
return out;
417+
}
418+
419+
@Override
420+
public String getString(long i) {
421+
return new String(getBytes(i), StandardCharsets.UTF_8);
422+
}
423+
424+
@Override
425+
public void forEachByteLength(IntConsumer c) {
426+
long n = length;
427+
for (long i = 0; i < n; i++) {
428+
c.accept(views.get(PTypeIO.LE_INT, i * VIEW_SIZE));
429+
}
430+
}
431+
432+
@Override
433+
public VarBinArray truncate(long rows) {
434+
if (rows >= length) {
435+
return this;
436+
}
437+
return new ViewMode(dtype, rows, views.asSlice(0, rows * VIEW_SIZE), dataBufs);
438+
}
439+
}
335440
}

reader/src/main/java/io/github/dfa1/vortex/reader/decode/DictEncodingDecoder.java

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -158,13 +158,11 @@ private static Array decodeUtf8DictProto(DecodeContext ctx, ByteBuffer metaBuf)
158158
DType codesDtype = new DType.Primitive(codePType, false);
159159
MemorySegment codesBuf = ctx.decodeChildSegment(0, codesDtype, n);
160160

161-
Array valuesArr = ctx.decodeChild(1, ctx.dtype(), dictSize);
162-
VarBinArray.OffsetMode varBinValues = (VarBinArray.OffsetMode) valuesArr;
163-
MemorySegment dictBytes = varBinValues.bytesSegment();
164-
MemorySegment dictOffsets = varBinValues.offsetsSegment();
161+
VarBinArray valuesArr = (VarBinArray) ctx.decodeChild(1, ctx.dtype(), dictSize);
162+
VarBinArray.OffsetMode dictValues = VarBinArray.toOffsetMode(valuesArr, ctx.arena());
165163

166164
return VarBinArray.ofDict(ctx.dtype(), n,
167-
dictBytes, dictOffsets, PType.I64,
165+
dictValues.bytesSegment(), dictValues.offsetsSegment(), PType.I64,
168166
codesBuf, codePType);
169167
}
170168

reader/src/main/java/io/github/dfa1/vortex/reader/decode/RunEndEncodingDecoder.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,8 @@ public Array decode(DecodeContext ctx) {
6868
Array endsArr = ctx.decodeChild(0, endsDtype, numRuns);
6969

7070
if (ctx.dtype() instanceof DType.Utf8 || ctx.dtype() instanceof DType.Binary) {
71-
Array valuesArr = ctx.decodeChild(1, ctx.dtype(), numRuns);
72-
return expandStrings(endsArr, (VarBinArray.OffsetMode) valuesArr, endsPtype, numRuns, offset, n, ctx.dtype(), ctx.arena());
71+
VarBinArray valuesArr = (VarBinArray) ctx.decodeChild(1, ctx.dtype(), numRuns);
72+
return expandStrings(endsArr, VarBinArray.toOffsetMode(valuesArr, ctx.arena()), endsPtype, numRuns, offset, n, ctx.dtype(), ctx.arena());
7373
}
7474

7575
if (ctx.dtype() instanceof DType.Bool) {

reader/src/main/java/io/github/dfa1/vortex/reader/decode/SparseEncodingDecoder.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,8 @@ private static Array decodeVarBin(
154154

155155
DType indicesDtype = new DType.Primitive(indicesPtype, false);
156156
MemorySegment idxSeg = ctx.decodeChildSegment(0, indicesDtype, numPatches);
157-
VarBinArray.OffsetMode varBin = (VarBinArray.OffsetMode) ctx.decodeChild(1, ctx.dtype(), numPatches);
157+
VarBinArray rawValues = (VarBinArray) ctx.decodeChild(1, ctx.dtype(), numPatches);
158+
VarBinArray.OffsetMode varBin = VarBinArray.toOffsetMode(rawValues, ctx.arena());
158159
MemorySegment valBytes = varBin.bytesSegment();
159160
MemorySegment valOffsets = varBin.offsetsSegment();
160161
PType valOffPtype = varBin.offsetsPtype();
@@ -222,4 +223,5 @@ private static long scalarToLong(ScalarValue scalar) {
222223
}
223224
return 0L;
224225
}
226+
225227
}
Lines changed: 4 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,16 @@
11
package io.github.dfa1.vortex.reader.decode;
22

33
import io.github.dfa1.vortex.core.DType;
4-
import io.github.dfa1.vortex.core.PType;
54
import io.github.dfa1.vortex.core.VortexException;
65
import io.github.dfa1.vortex.reader.array.Array;
76
import io.github.dfa1.vortex.reader.array.VarBinArray;
87
import io.github.dfa1.vortex.encoding.EncodingId;
9-
import io.github.dfa1.vortex.encoding.PTypeIO;
108

119
import java.lang.foreign.MemorySegment;
1210

1311
/// Read-only decoder for {@code vortex.varbinview} (Apache Arrow StringView/BinaryView).
1412
public final class VarBinViewEncodingDecoder implements EncodingDecoder {
1513

16-
private static final int MAX_INLINED_SIZE = 12;
17-
private static final int VIEW_SIZE = 16;
18-
1914
/// Public no-arg constructor required by {@link java.util.ServiceLoader}.
2015
public VarBinViewEncodingDecoder() {
2116
}
@@ -43,39 +38,14 @@ public Array decode(DecodeContext ctx) {
4338
"expected at least 1 buffer (views), got 0");
4439
}
4540

41+
// Lazy path: keep views + data buffers as MemorySegment slices; per-row
42+
// accessors resolve on demand via VarBinArray.ViewMode. No copy, no concat,
43+
// no flat byte buffer allocation.
4644
MemorySegment viewsBuf = ctx.buffer(numBufs - 1);
4745
MemorySegment[] dataBufs = new MemorySegment[numBufs - 1];
4846
for (int i = 0; i < dataBufs.length; i++) {
4947
dataBufs[i] = ctx.buffer(i);
5048
}
51-
52-
long n = ctx.rowCount();
53-
54-
long totalBytes = 0;
55-
for (long i = 0; i < n; i++) {
56-
long size = Integer.toUnsignedLong(viewsBuf.get(PTypeIO.LE_INT, i * VIEW_SIZE));
57-
totalBytes += size;
58-
}
59-
60-
MemorySegment outBytes = ctx.arena().allocate(totalBytes > 0 ? totalBytes : 1);
61-
MemorySegment outOffsets = ctx.arena().allocate((n + 1) * Long.BYTES, Long.BYTES);
62-
63-
long bytePos = 0;
64-
outOffsets.setAtIndex(PTypeIO.LE_LONG, 0, 0L);
65-
for (long i = 0; i < n; i++) {
66-
long viewOff = i * VIEW_SIZE;
67-
long size = Integer.toUnsignedLong(viewsBuf.get(PTypeIO.LE_INT, viewOff));
68-
if (size <= MAX_INLINED_SIZE) {
69-
MemorySegment.copy(viewsBuf, viewOff + 4, outBytes, bytePos, size);
70-
} else {
71-
int bufferIndex = viewsBuf.get(PTypeIO.LE_INT, viewOff + 8);
72-
long srcOffset = Integer.toUnsignedLong(viewsBuf.get(PTypeIO.LE_INT, viewOff + 12));
73-
MemorySegment.copy(dataBufs[bufferIndex], srcOffset, outBytes, bytePos, size);
74-
}
75-
bytePos += size;
76-
outOffsets.setAtIndex(PTypeIO.LE_LONG, i + 1, bytePos);
77-
}
78-
79-
return new VarBinArray.OffsetMode(ctx.dtype(), n, outBytes.asReadOnly(), outOffsets, PType.I64);
49+
return new VarBinArray.ViewMode(ctx.dtype(), ctx.rowCount(), viewsBuf, dataBufs);
8050
}
8151
}

0 commit comments

Comments
 (0)