Skip to content

Commit a9e4137

Browse files
dfa1claude
andcommitted
fix: lazy dict decode for I8/U8/I16/U16 value columns (#206)
Files from the Python bindings (vortex-data 0.69.0) dict-encode narrow-integer columns; the dict values land as U8/U16 primitives and buildLazyDictPrimitive threw "unsupported ptype for lazy dict". Add DictByteArray and DictShortArray mirroring the existing lazy dict records: raw-signed storage semantics (unsigned widening stays in the ByteArray/ShortArray getInt defaults, keyed on dtype), hoisted code-width switches in materialize/forEach/fold, and the same validateCodes construction guard. F16 remains the only unsupported value ptype. On the Raincloud corpus this flips uci-bike-sharing-dataset to a full value-for-value pass and unblocks the kepler/penguins scans far enough to expose two pre-existing bugs, reclassified in the conformance matrix: nullable ALP columns dropping row validity (#210) and CSV export lacking a NullArray arm (#211). Closes #206 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 7945318 commit a9e4137

7 files changed

Lines changed: 562 additions & 6 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Fixed
11+
12+
- Lazy dict decode now covers I8/U8/I16/U16 value columns (`DictByteArray`, `DictShortArray`) — files from the Python bindings dict-encode narrow-integer columns, which previously failed to scan with `unsupported ptype for lazy dict`. ([#206](https://github.com/dfa1/vortex-java/issues/206))
13+
1014
### Added
1115

1216
- Real-world conformance suite against the [Raincloud](https://github.com/spiraldb/raincloud) corpus: 247 public datasets whose Vortex files are written by the Python bindings, each validated value-for-value against its Parquet sibling. `scripts/hydrate-raincloud-corpus.sh` hydrates any subset (cache → mirror → local build), `RaincloudConformanceIntegrationTest` tests whatever is hydrated against the checked-in per-dataset status matrix, and a weekly workflow runs a size-capped sweep. First triage found four reader gaps on real data, including one silent-corruption bug (unsigned integers rendered as signed). ([#205](https://github.com/dfa1/vortex-java/issues/205))

docs/compatibility.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,11 +62,14 @@ Per-slug status lives in `integration/src/test/resources/raincloud/expected-stat
6262
(`ok` must pass; `gap:<issue>` must still fail, so a fix flips the entry in the same change;
6363
`untriaged` runs and reports without failing the build). A scheduled workflow
6464
(`raincloud-conformance.yml`) hydrates a size-capped subset weekly. Current triage —
65-
4 `ok`, 6 known gaps: lazy dict U8/U16 values
66-
([#206](https://github.com/dfa1/vortex-java/issues/206)), nested struct columns in scan
65+
5 `ok`, 5 known gaps: nested struct columns in scan
6766
([#207](https://github.com/dfa1/vortex-java/issues/207)), unsigned integers rendered signed
6867
([#208](https://github.com/dfa1/vortex-java/issues/208) — silent corruption), RLE over F64
69-
([#209](https://github.com/dfa1/vortex-java/issues/209)); 237 slugs untriaged.
68+
([#209](https://github.com/dfa1/vortex-java/issues/209)), nullable ALP validity dropped
69+
([#210](https://github.com/dfa1/vortex-java/issues/210) — silent corruption), all-null
70+
columns in CSV export ([#211](https://github.com/dfa1/vortex-java/issues/211));
71+
237 slugs untriaged. Fixed so far by this suite: lazy dict U8/U16 values
72+
([#206](https://github.com/dfa1/vortex-java/issues/206)).
7073

7174
## Encodings
7275

integration/src/test/resources/raincloud/expected-status.csv

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ humaneval,untriaged
124124
insurance,untriaged
125125
international-football-results-from-1872-to-2017,untriaged
126126
iowa-liquor-sales,untriaged
127-
kepler-exoplanet-search-results,gap:206
127+
kepler-exoplanet-search-results,gap:211
128128
kepler-labelled-time-series-data,untriaged
129129
laion-400m,untriaged
130130
librispeech-test-clean,untriaged
@@ -166,7 +166,7 @@ osmi-mental-health-in-tech-2020,untriaged
166166
osmi-mental-health-in-tech-2021,untriaged
167167
osmi-mental-health-in-tech-2022,untriaged
168168
osmi-mental-health-in-tech-2023,untriaged
169-
palmer-archipelago-antarctica-penguin-data,gap:206
169+
palmer-archipelago-antarctica-penguin-data,gap:210
170170
peoples-speech-clean-validation,untriaged
171171
personal-key-indicators-of-heart-disease,untriaged
172172
pleias-synth,untriaged
@@ -194,7 +194,7 @@ uci-auto-mpg,untriaged
194194
uci-automobile,untriaged
195195
uci-bank-marketing,untriaged
196196
uci-beijing-multi-site-air-quality,untriaged
197-
uci-bike-sharing-dataset,gap:206
197+
uci-bike-sharing-dataset,ok
198198
uci-breast-cancer,untriaged
199199
uci-breast-cancer-wisconsin-diagnostic,untriaged
200200
uci-breast-cancer-wisconsin-original,untriaged
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
package io.github.dfa1.vortex.reader.array;
2+
3+
import io.github.dfa1.vortex.core.model.DType;
4+
import io.github.dfa1.vortex.core.error.VortexException;
5+
6+
import java.lang.foreign.MemorySegment;
7+
import java.lang.foreign.SegmentAllocator;
8+
import java.lang.foreign.ValueLayout;
9+
import java.util.function.LongBinaryOperator;
10+
11+
/// Dict-encoded [ByteArray] view. ADR 0012 shape.
12+
///
13+
/// Stores `values` (the dictionary pool) and `codes` (one index per
14+
/// row into `values`). Scalar access resolves on demand:
15+
/// `getByte(i) = values.getByte(codes.getCode(i))`. Per ADR 0012, this
16+
/// preserves zero-copy on dict-encoded categorical columns.
17+
///
18+
/// The `codes` array is typed as [Array] because the codes ptype
19+
/// varies with dictionary size — U8/U16/U32/U64 backed by
20+
/// [ByteArray]/[ShortArray]/[IntArray]/[LongArray].
21+
///
22+
/// @param dtype logical element type (matches `values.dtype()`)
23+
/// @param length total logical row count (matches `codes.length()`)
24+
/// @param values dictionary pool — element at code `c` is `values.getByte(c)`
25+
/// @param codes per-row index into `values`; must be one of
26+
/// [ByteArray], [ShortArray], [IntArray], [LongArray]
27+
public record DictByteArray(DType dtype, long length, ByteArray values, Array codes) implements ByteArray {
28+
29+
/// Builds a [DictByteArray], validating that `codes` is one of the
30+
/// four narrow-int code array types and that its length matches `length`.
31+
///
32+
/// @param dtype logical element type
33+
/// @param length total logical row count
34+
/// @param values dictionary pool
35+
/// @param codes per-row code array (must be [ByteArray], [ShortArray],
36+
/// [IntArray], or [LongArray])
37+
/// @return a new [DictByteArray]
38+
/// @throws VortexException if `codes` is not a supported code-array type or
39+
/// its length does not equal `length`
40+
public static DictByteArray of(DType dtype, long length, ByteArray values, Array codes) {
41+
DictArrays.validateCodes(codes, length);
42+
return new DictByteArray(dtype, length, values, codes);
43+
}
44+
45+
@Override
46+
public byte getByte(long i) {
47+
return values.getByte(DictArrays.readCode(codes, i));
48+
}
49+
50+
/// Materializes by gathering one dictionary value per code into a fresh
51+
/// one-byte-per-element segment. The codes switch is hoisted outside the loop so
52+
/// each branch is a uniform gather over a single code width.
53+
///
54+
/// @param arena allocator for the output segment
55+
/// @return a read-only segment of `length()` gathered bytes
56+
/// @throws VortexException if `codes` is not a supported code-array type
57+
@Override
58+
public MemorySegment materialize(SegmentAllocator arena) {
59+
long n = length;
60+
MemorySegment dst = arena.allocate(n);
61+
ByteArray vals = values;
62+
switch (codes) {
63+
case ByteArray ba -> {
64+
for (long i = 0; i < n; i++) {
65+
dst.set(ValueLayout.JAVA_BYTE, i, vals.getByte(Byte.toUnsignedLong(ba.getByte(i))));
66+
}
67+
}
68+
case ShortArray sa -> {
69+
for (long i = 0; i < n; i++) {
70+
dst.set(ValueLayout.JAVA_BYTE, i, vals.getByte(Short.toUnsignedLong(sa.getShort(i))));
71+
}
72+
}
73+
case IntArray ia -> {
74+
for (long i = 0; i < n; i++) {
75+
dst.set(ValueLayout.JAVA_BYTE, i, vals.getByte(Integer.toUnsignedLong(ia.getInt(i))));
76+
}
77+
}
78+
case LongArray la -> {
79+
for (long i = 0; i < n; i++) {
80+
dst.set(ValueLayout.JAVA_BYTE, i, vals.getByte(la.getLong(i)));
81+
}
82+
}
83+
default -> throw new VortexException("DictByteArray: invalid codes type: "
84+
+ codes.getClass().getSimpleName());
85+
}
86+
return dst.asReadOnly();
87+
}
88+
89+
@Override
90+
public void forEachByte(ByteConsumer cons) {
91+
long n = length;
92+
ByteArray vals = values;
93+
switch (codes) {
94+
case ByteArray ba -> {
95+
for (long i = 0; i < n; i++) {
96+
cons.accept(vals.getByte(Byte.toUnsignedLong(ba.getByte(i))));
97+
}
98+
}
99+
case ShortArray sa -> {
100+
for (long i = 0; i < n; i++) {
101+
cons.accept(vals.getByte(Short.toUnsignedLong(sa.getShort(i))));
102+
}
103+
}
104+
case IntArray ia -> {
105+
for (long i = 0; i < n; i++) {
106+
cons.accept(vals.getByte(Integer.toUnsignedLong(ia.getInt(i))));
107+
}
108+
}
109+
case LongArray la -> {
110+
for (long i = 0; i < n; i++) {
111+
cons.accept(vals.getByte(la.getLong(i)));
112+
}
113+
}
114+
default -> throw new VortexException("DictByteArray: invalid codes type: "
115+
+ codes.getClass().getSimpleName());
116+
}
117+
}
118+
119+
@Override
120+
public long fold(long identity, LongBinaryOperator op) {
121+
long n = length;
122+
ByteArray vals = values;
123+
long result = identity;
124+
switch (codes) {
125+
case ByteArray ba -> {
126+
for (long i = 0; i < n; i++) {
127+
result = op.applyAsLong(result, vals.getByte(Byte.toUnsignedLong(ba.getByte(i))));
128+
}
129+
}
130+
case ShortArray sa -> {
131+
for (long i = 0; i < n; i++) {
132+
result = op.applyAsLong(result, vals.getByte(Short.toUnsignedLong(sa.getShort(i))));
133+
}
134+
}
135+
case IntArray ia -> {
136+
for (long i = 0; i < n; i++) {
137+
result = op.applyAsLong(result, vals.getByte(Integer.toUnsignedLong(ia.getInt(i))));
138+
}
139+
}
140+
case LongArray la -> {
141+
for (long i = 0; i < n; i++) {
142+
result = op.applyAsLong(result, vals.getByte(la.getLong(i)));
143+
}
144+
}
145+
default -> throw new VortexException("DictByteArray: invalid codes type: "
146+
+ codes.getClass().getSimpleName());
147+
}
148+
return result;
149+
}
150+
}
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
package io.github.dfa1.vortex.reader.array;
2+
3+
import io.github.dfa1.vortex.core.model.DType;
4+
import io.github.dfa1.vortex.core.error.VortexException;
5+
import io.github.dfa1.vortex.core.io.VortexFormat;
6+
7+
import java.lang.foreign.MemorySegment;
8+
import java.lang.foreign.SegmentAllocator;
9+
import java.util.function.LongBinaryOperator;
10+
11+
/// Dict-encoded [ShortArray] view. ADR 0012 shape.
12+
///
13+
/// Stores `values` (the dictionary pool) and `codes` (one index per
14+
/// row into `values`). Scalar access resolves on demand:
15+
/// `getShort(i) = values.getShort(codes.getCode(i))`. Per ADR 0012, this
16+
/// preserves zero-copy on dict-encoded categorical columns.
17+
///
18+
/// The `codes` array is typed as [Array] because the codes ptype
19+
/// varies with dictionary size — U8/U16/U32/U64 backed by
20+
/// [ByteArray]/[ShortArray]/[IntArray]/[LongArray].
21+
///
22+
/// @param dtype logical element type (matches `values.dtype()`)
23+
/// @param length total logical row count (matches `codes.length()`)
24+
/// @param values dictionary pool — element at code `c` is `values.getShort(c)`
25+
/// @param codes per-row index into `values`; must be one of
26+
/// [ByteArray], [ShortArray], [IntArray], [LongArray]
27+
public record DictShortArray(DType dtype, long length, ShortArray values, Array codes) implements ShortArray {
28+
29+
/// Builds a [DictShortArray], validating that `codes` is one of the
30+
/// four narrow-int code array types and that its length matches `length`.
31+
///
32+
/// @param dtype logical element type
33+
/// @param length total logical row count
34+
/// @param values dictionary pool
35+
/// @param codes per-row code array (must be [ByteArray], [ShortArray],
36+
/// [IntArray], or [LongArray])
37+
/// @return a new [DictShortArray]
38+
/// @throws VortexException if `codes` is not a supported code-array type or
39+
/// its length does not equal `length`
40+
public static DictShortArray of(DType dtype, long length, ShortArray values, Array codes) {
41+
DictArrays.validateCodes(codes, length);
42+
return new DictShortArray(dtype, length, values, codes);
43+
}
44+
45+
@Override
46+
public short getShort(long i) {
47+
return values.getShort(DictArrays.readCode(codes, i));
48+
}
49+
50+
/// Materializes by gathering one dictionary value per code into a fresh
51+
/// little-endian `i16` segment. The codes switch is hoisted outside the loop so
52+
/// each branch is a uniform gather over a single code width.
53+
///
54+
/// @param arena allocator for the output segment
55+
/// @return a read-only little-endian `i16` segment of gathered values
56+
/// @throws VortexException if `codes` is not a supported code-array type
57+
@Override
58+
public MemorySegment materialize(SegmentAllocator arena) {
59+
long n = length;
60+
MemorySegment dst = arena.allocate(n * 2L, 2);
61+
ShortArray vals = values;
62+
switch (codes) {
63+
case ByteArray ba -> {
64+
for (long i = 0; i < n; i++) {
65+
dst.setAtIndex(VortexFormat.LE_SHORT, i, vals.getShort(Byte.toUnsignedLong(ba.getByte(i))));
66+
}
67+
}
68+
case ShortArray sa -> {
69+
for (long i = 0; i < n; i++) {
70+
dst.setAtIndex(VortexFormat.LE_SHORT, i, vals.getShort(Short.toUnsignedLong(sa.getShort(i))));
71+
}
72+
}
73+
case IntArray ia -> {
74+
for (long i = 0; i < n; i++) {
75+
dst.setAtIndex(VortexFormat.LE_SHORT, i, vals.getShort(Integer.toUnsignedLong(ia.getInt(i))));
76+
}
77+
}
78+
case LongArray la -> {
79+
for (long i = 0; i < n; i++) {
80+
dst.setAtIndex(VortexFormat.LE_SHORT, i, vals.getShort(la.getLong(i)));
81+
}
82+
}
83+
default -> throw new VortexException("DictShortArray: invalid codes type: "
84+
+ codes.getClass().getSimpleName());
85+
}
86+
return dst.asReadOnly();
87+
}
88+
89+
@Override
90+
public void forEachShort(ShortConsumer cons) {
91+
long n = length;
92+
ShortArray vals = values;
93+
switch (codes) {
94+
case ByteArray ba -> {
95+
for (long i = 0; i < n; i++) {
96+
cons.accept(vals.getShort(Byte.toUnsignedLong(ba.getByte(i))));
97+
}
98+
}
99+
case ShortArray sa -> {
100+
for (long i = 0; i < n; i++) {
101+
cons.accept(vals.getShort(Short.toUnsignedLong(sa.getShort(i))));
102+
}
103+
}
104+
case IntArray ia -> {
105+
for (long i = 0; i < n; i++) {
106+
cons.accept(vals.getShort(Integer.toUnsignedLong(ia.getInt(i))));
107+
}
108+
}
109+
case LongArray la -> {
110+
for (long i = 0; i < n; i++) {
111+
cons.accept(vals.getShort(la.getLong(i)));
112+
}
113+
}
114+
default -> throw new VortexException("DictShortArray: invalid codes type: "
115+
+ codes.getClass().getSimpleName());
116+
}
117+
}
118+
119+
@Override
120+
public long fold(long identity, LongBinaryOperator op) {
121+
long n = length;
122+
ShortArray vals = values;
123+
long result = identity;
124+
switch (codes) {
125+
case ByteArray ba -> {
126+
for (long i = 0; i < n; i++) {
127+
result = op.applyAsLong(result, vals.getShort(Byte.toUnsignedLong(ba.getByte(i))));
128+
}
129+
}
130+
case ShortArray sa -> {
131+
for (long i = 0; i < n; i++) {
132+
result = op.applyAsLong(result, vals.getShort(Short.toUnsignedLong(sa.getShort(i))));
133+
}
134+
}
135+
case IntArray ia -> {
136+
for (long i = 0; i < n; i++) {
137+
result = op.applyAsLong(result, vals.getShort(Integer.toUnsignedLong(ia.getInt(i))));
138+
}
139+
}
140+
case LongArray la -> {
141+
for (long i = 0; i < n; i++) {
142+
result = op.applyAsLong(result, vals.getShort(la.getLong(i)));
143+
}
144+
}
145+
default -> throw new VortexException("DictShortArray: invalid codes type: "
146+
+ codes.getClass().getSimpleName());
147+
}
148+
return result;
149+
}
150+
}

reader/src/main/java/io/github/dfa1/vortex/reader/layout/DictLayoutDecoder.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,18 @@
1010
import io.github.dfa1.vortex.core.model.LayoutId;
1111
import io.github.dfa1.vortex.core.model.PType;
1212
import io.github.dfa1.vortex.reader.array.Array;
13+
import io.github.dfa1.vortex.reader.array.ByteArray;
14+
import io.github.dfa1.vortex.reader.array.DictByteArray;
1315
import io.github.dfa1.vortex.reader.array.DictDoubleArray;
1416
import io.github.dfa1.vortex.reader.array.DictFloatArray;
1517
import io.github.dfa1.vortex.reader.array.DictIntArray;
1618
import io.github.dfa1.vortex.reader.array.DictLongArray;
19+
import io.github.dfa1.vortex.reader.array.DictShortArray;
1720
import io.github.dfa1.vortex.reader.array.DoubleArray;
1821
import io.github.dfa1.vortex.reader.array.FloatArray;
1922
import io.github.dfa1.vortex.reader.array.IntArray;
2023
import io.github.dfa1.vortex.reader.array.LongArray;
24+
import io.github.dfa1.vortex.reader.array.ShortArray;
2125
import io.github.dfa1.vortex.reader.array.MaskedArray;
2226
import io.github.dfa1.vortex.reader.array.VarBinArray;
2327

@@ -132,8 +136,11 @@ private static Array buildLazyDictPrimitive(DType.Primitive dtype, long n, Array
132136
return switch (ptype) {
133137
case I64, U64 -> DictLongArray.of(dtype, n, (LongArray) valuesData, codesData);
134138
case I32, U32 -> DictIntArray.of(dtype, n, (IntArray) valuesData, codesData);
139+
case I16, U16 -> DictShortArray.of(dtype, n, (ShortArray) valuesData, codesData);
140+
case I8, U8 -> DictByteArray.of(dtype, n, (ByteArray) valuesData, codesData);
135141
case F64 -> DictDoubleArray.of(dtype, n, (DoubleArray) valuesData, codesData);
136142
case F32 -> DictFloatArray.of(dtype, n, (FloatArray) valuesData, codesData);
143+
// F16 has no Array subtype yet
137144
default -> throw new VortexException(EncodingId.VORTEX_DICT,
138145
"layout: unsupported ptype for lazy dict: " + ptype);
139146
};

0 commit comments

Comments
 (0)