Skip to content

Commit dc450ee

Browse files
committed
refactor: introduce VortexException with optional CodecId
Unchecked exception for unrecoverable Vortex errors (malformed file, unsupported feature, codec failure). Carries an optional CodecId so callers can attribute decode failures without parsing the message. Migrate decode-path errors across 12 codecs, CodecRegistry, CodecId, PostscriptParser, VortexReader, and ScanIterator from IllegalStateException / IllegalArgumentException / UnsupportedOperationException / IOException to VortexException. Codec sites pass their CodecId. DecodeContext.decodeChild drops vestigial throws IOException. VortexReader.open keeps throws IOException only for real FileChannel ops; format errors are now unchecked. Writer untouched (missing-column is input validation). Close Code quality section in TODO.
1 parent e5e7a2a commit dc450ee

20 files changed

Lines changed: 155 additions & 115 deletions

TODO.md

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -86,13 +86,6 @@
8686
- Arrow JVM uses `sun.misc.Unsafe` / Netty internally; keeping it in a separate module means
8787
the core library stays Unsafe-free
8888

89-
## Code quality
90-
91-
- [ ] Use a dedicated `VortexException` (unchecked) instead of `IOException` for unrecoverable errors
92-
- [ ] Introduce `CodecType` enum to replace stringly-typed error messages
93-
- [ ] Avoid allocating intermediate `ByteBuffer` — always use `MemorySegment` from arena
94-
- [ ] Domain primitives: `RowCount`, `Limit`/`Unlimited` (cannot be zero)
95-
9689
## Skills
9790

9891
- [ ] Keep `.claude/skills/improve-performance.md` and `.claude/skills/review-performance.md` in sync with
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package io.github.dfa1.vortex.core;
2+
3+
import io.github.dfa1.vortex.encoding.CodecId;
4+
5+
import java.util.Optional;
6+
7+
/// Unrecoverable Vortex error: malformed file, unsupported feature, or codec failure.
8+
///
9+
/// Carries an optional [CodecId] so callers can attribute decode failures to a specific
10+
/// encoding without parsing the message.
11+
public final class VortexException extends RuntimeException {
12+
13+
private final CodecId codecId;
14+
15+
public VortexException(String message) {
16+
super(message);
17+
this.codecId = null;
18+
}
19+
20+
public VortexException(String message, Throwable cause) {
21+
super(message, cause);
22+
this.codecId = null;
23+
}
24+
25+
public VortexException(CodecId codecId, String message) {
26+
super(prefix(codecId) + message);
27+
this.codecId = codecId;
28+
}
29+
30+
public VortexException(CodecId codecId, String message, Throwable cause) {
31+
super(prefix(codecId) + message, cause);
32+
this.codecId = codecId;
33+
}
34+
35+
public Optional<CodecId> codecId() {
36+
return Optional.ofNullable(codecId);
37+
}
38+
39+
private static String prefix(CodecId codecId) {
40+
return codecId == null ? "" : codecId.id() + ": ";
41+
}
42+
}

core/src/main/java/io/github/dfa1/vortex/encoding/AlpCodec.java

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import io.github.dfa1.vortex.core.ArrayStats;
88
import io.github.dfa1.vortex.core.DType;
99
import io.github.dfa1.vortex.core.PType;
10+
import io.github.dfa1.vortex.core.VortexException;
1011

1112
import java.lang.foreign.MemorySegment;
1213
import java.lang.foreign.ValueLayout;
@@ -71,7 +72,7 @@ private static long readUnsigned(MemorySegment seg, long i, PType ptype) {
7172
case U16 -> Short.toUnsignedLong(seg.get(LE_SHORT, i * 2));
7273
case U32 -> Integer.toUnsignedLong(seg.get(LE_INT, i * 4));
7374
case U64 -> seg.get(LE_LONG, i * 8);
74-
default -> throw new IllegalStateException("vortex.alp: non-unsigned patch index ptype " + ptype);
75+
default -> throw new VortexException(CodecId.VORTEX_ALP, "non-unsigned patch index ptype " + ptype);
7576
};
7677
}
7778

@@ -95,17 +96,17 @@ public EncodeResult encode(DType dtype, Object data) {
9596
public Array decode(DecodeContext ctx) {
9697
ByteBuffer rawMeta = ctx.metadata();
9798
if (rawMeta == null) {
98-
throw new IllegalStateException("vortex.alp: missing metadata");
99+
throw new VortexException(CodecId.VORTEX_ALP, "missing metadata");
99100
}
100101
EncodingProtos.ALPMetadata meta;
101102
try {
102103
meta = EncodingProtos.ALPMetadata.parseFrom(rawMeta.duplicate());
103104
} catch (InvalidProtocolBufferException e) {
104-
throw new IllegalStateException("vortex.alp: invalid metadata", e);
105+
throw new VortexException(CodecId.VORTEX_ALP, "invalid metadata", e);
105106
}
106107

107108
if (!(ctx.dtype() instanceof DType.Primitive p)) {
108-
throw new IllegalStateException("vortex.alp: expected primitive dtype, got " + ctx.dtype());
109+
throw new VortexException(CodecId.VORTEX_ALP, "expected primitive dtype, got " + ctx.dtype());
109110
}
110111

111112
int expE = meta.getExpE();
@@ -116,7 +117,7 @@ public Array decode(DecodeContext ctx) {
116117
return switch (ptype) {
117118
case F64 -> decodeF64(ctx, meta, expE, expF, n);
118119
case F32 -> decodeF32(ctx, meta, expE, expF, n);
119-
default -> throw new IllegalStateException("vortex.alp: unsupported dtype " + ptype);
120+
default -> throw new VortexException(CodecId.VORTEX_ALP, "unsupported dtype " + ptype);
120121
};
121122
}
122123

core/src/main/java/io/github/dfa1/vortex/encoding/BitpackedCodec.java

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import io.github.dfa1.vortex.core.ArrayStats;
99
import io.github.dfa1.vortex.core.DType;
1010
import io.github.dfa1.vortex.core.PType;
11+
import io.github.dfa1.vortex.core.VortexException;
1112

1213
import java.lang.foreign.MemorySegment;
1314
import java.lang.foreign.ValueLayout;
@@ -87,7 +88,7 @@ private static void fastlanesUnpackToSeg(
8788
case 16 -> unpackLoop16(buf, bitWidth, offset, rowCount, output);
8889
case 32 -> unpackLoop32(buf, bitWidth, offset, rowCount, output);
8990
case 64 -> unpackLoop64(buf, bitWidth, offset, rowCount, output);
90-
default -> throw new IllegalArgumentException("unsupported typeBits: " + typeBits);
91+
default -> throw new VortexException(CodecId.FASTLANES_BITPACKED, "unsupported typeBits: " + typeBits);
9192
}
9293
}
9394

@@ -380,7 +381,7 @@ private static long readWordFromBuf(byte[] buf, int off, int typeBits) {
380381
| ((buf[off + 6] & 0xFF) << 16) | ((buf[off + 7] & 0xFF) << 24));
381382
yield lo | (hi << 32);
382383
}
383-
default -> throw new IllegalArgumentException("unsupported typeBits: " + typeBits);
384+
default -> throw new VortexException(CodecId.FASTLANES_BITPACKED, "unsupported typeBits: " + typeBits);
384385
};
385386
}
386387

@@ -407,7 +408,7 @@ private static void writeWordToBuf(byte[] buf, int off, long value, int typeBits
407408
buf[off + 6] = (byte) (value >>> 48);
408409
buf[off + 7] = (byte) (value >>> 56);
409410
}
410-
default -> throw new IllegalArgumentException("unsupported typeBits: " + typeBits);
411+
default -> throw new VortexException(CodecId.FASTLANES_BITPACKED, "unsupported typeBits: " + typeBits);
411412
}
412413
}
413414

@@ -462,7 +463,7 @@ private static long[] toLongs(Object data, PType ptype) {
462463
yield r;
463464
}
464465
case I64, U64 -> (long[]) data;
465-
default -> throw new UnsupportedOperationException("unsupported ptype: " + ptype);
466+
default -> throw new VortexException(CodecId.FASTLANES_BITPACKED, "unsupported ptype: " + ptype);
466467
};
467468
}
468469

@@ -559,14 +560,14 @@ public EncodeResult encode(DType dtype, Object data) {
559560
public Array decode(DecodeContext ctx) {
560561
ByteBuffer rawMeta = ctx.metadata();
561562
if (rawMeta == null) {
562-
throw new IllegalStateException("fastlanes.bitpacked: missing metadata");
563+
throw new VortexException(CodecId.FASTLANES_BITPACKED, "missing metadata");
563564
}
564565

565566
EncodingProtos.BitPackedMetadata meta;
566567
try {
567568
meta = EncodingProtos.BitPackedMetadata.parseFrom(rawMeta.duplicate());
568569
} catch (InvalidProtocolBufferException e) {
569-
throw new IllegalStateException("fastlanes.bitpacked: invalid metadata", e);
570+
throw new VortexException(CodecId.FASTLANES_BITPACKED, "invalid metadata", e);
570571
}
571572

572573
int bitWidth = meta.getBitWidth();
@@ -602,8 +603,8 @@ private static long readUnsignedIdx(MemorySegment seg, long i, PType ptype) {
602603
case U16 -> Short.toUnsignedLong(seg.get(LE_SHORT, i * 2));
603604
case U32 -> Integer.toUnsignedLong(seg.get(LE_INT, i * 4));
604605
case U64 -> seg.get(LE_LONG, i * 8);
605-
default -> throw new IllegalStateException(
606-
"fastlanes.bitpacked: non-unsigned patch index ptype " + ptype);
606+
default -> throw new VortexException(CodecId.FASTLANES_BITPACKED,
607+
"non-unsigned patch index ptype " + ptype);
607608
};
608609
}
609610

@@ -630,8 +631,8 @@ private static void applyPatches(DecodeContext ctx, EncodingProtos.PatchesMetada
630631
for (long i = 0; i < numPatches; i++) {
631632
long absIdx = readUnsignedIdx(idxSeg, i, idxPtype) - offset;
632633
if (absIdx < 0 || absIdx >= n) {
633-
throw new IllegalStateException(
634-
"fastlanes.bitpacked: patch index " + absIdx + " out of range [0," + n + ")");
634+
throw new VortexException(CodecId.FASTLANES_BITPACKED,
635+
"patch index " + absIdx + " out of range [0," + n + ")");
635636
}
636637
MemorySegment.copy(valSeg, i * elemBytes, out, absIdx * elemBytes, elemBytes);
637638
}

core/src/main/java/io/github/dfa1/vortex/encoding/CodecId.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package io.github.dfa1.vortex.encoding;
22

3+
import io.github.dfa1.vortex.core.VortexException;
4+
35
import java.util.Map;
46
import java.util.function.Function;
57
import java.util.stream.Collectors;
@@ -42,7 +44,7 @@ public enum CodecId {
4244
public static CodecId from(String id) {
4345
CodecId result = LOOKUP.get(id);
4446
if (result == null) {
45-
throw new IllegalArgumentException("unknown encoding id: " + id);
47+
throw new VortexException("unknown encoding id: " + id);
4648
}
4749
return result;
4850
}

core/src/main/java/io/github/dfa1/vortex/encoding/CodecRegistry.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import io.github.dfa1.vortex.core.Array;
44
import io.github.dfa1.vortex.core.ArrayStats;
55
import io.github.dfa1.vortex.core.DType;
6+
import io.github.dfa1.vortex.core.VortexException;
67
import io.github.dfa1.vortex.fbs.Buffer;
78

89
import java.lang.foreign.Arena;
@@ -94,7 +95,7 @@ Array decode(DecodeContext ctx) {
9495
CodecId id = ctx.node().encodingId();
9596
Codec c = codecs.get(id);
9697
if (c == null) {
97-
throw new IllegalArgumentException("no codec for encoding: " + id);
98+
throw new VortexException(id, "no codec registered");
9899
}
99100
return c.decode(ctx);
100101
}

core/src/main/java/io/github/dfa1/vortex/encoding/ConstantCodec.java

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import io.github.dfa1.vortex.core.ArrayStats;
77
import io.github.dfa1.vortex.core.DType;
88
import io.github.dfa1.vortex.core.PType;
9+
import io.github.dfa1.vortex.core.VortexException;
910

1011
import java.lang.foreign.MemorySegment;
1112
import java.lang.foreign.ValueLayout;
@@ -30,8 +31,8 @@ private static long scalarToRawBits(ScalarProtos.ScalarValue scalar, PType ptype
3031
case F32_VALUE -> Float.floatToRawIntBits(scalar.getF32Value());
3132
case F64_VALUE -> Double.doubleToRawLongBits(scalar.getF64Value());
3233
case KIND_NOT_SET -> 0L;
33-
default -> throw new IllegalStateException(
34-
"vortex.constant: unexpected scalar kind " + scalar.getKindCase());
34+
default -> throw new VortexException(CodecId.VORTEX_CONSTANT,
35+
"unexpected scalar kind " + scalar.getKindCase());
3536
};
3637
}
3738

@@ -41,7 +42,7 @@ private static void writeRaw(ByteBuffer buf, PType ptype, long rawBits) {
4142
case 2 -> buf.putShort((short) rawBits);
4243
case 4 -> buf.putInt((int) rawBits);
4344
case 8 -> buf.putLong(rawBits);
44-
default -> throw new UnsupportedOperationException("vortex.constant: unsupported ptype " + ptype);
45+
default -> throw new VortexException(CodecId.VORTEX_CONSTANT, "unsupported ptype " + ptype);
4546
}
4647
}
4748

@@ -66,7 +67,7 @@ public Array decode(DecodeContext ctx) {
6667
try {
6768
scalar = ScalarProtos.ScalarValue.parseFrom(scalarBytes);
6869
} catch (InvalidProtocolBufferException e) {
69-
throw new IllegalStateException("vortex.constant: invalid scalar value", e);
70+
throw new VortexException(CodecId.VORTEX_CONSTANT, "invalid scalar value", e);
7071
}
7172

7273
long n = ctx.rowCount();
@@ -76,7 +77,7 @@ public Array decode(DecodeContext ctx) {
7677
}
7778

7879
if (!(ctx.dtype() instanceof DType.Primitive p)) {
79-
throw new IllegalStateException("vortex.constant: unsupported dtype " + ctx.dtype());
80+
throw new VortexException(CodecId.VORTEX_CONSTANT, "unsupported dtype " + ctx.dtype());
8081
}
8182

8283
PType ptype = p.ptype();

core/src/main/java/io/github/dfa1/vortex/encoding/DecodeContext.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
import io.github.dfa1.vortex.core.Array;
44
import io.github.dfa1.vortex.core.DType;
55

6-
import java.io.IOException;
76
import java.lang.foreign.Arena;
87
import java.lang.foreign.MemorySegment;
98
import java.nio.ByteBuffer;
@@ -23,7 +22,7 @@ public record DecodeContext(
2322
Arena arena
2423
) {
2524
/// Recursively decode child `i` using the same segment buffers, registry and arena.
26-
public Array decodeChild(int i) throws IOException {
25+
public Array decodeChild(int i) {
2726
ArrayNode child = node.children()[i];
2827
var childCtx = new DecodeContext(child, dtype, rowCount, segmentBuffers, registry, arena);
2928
return registry.decode(childCtx);

core/src/main/java/io/github/dfa1/vortex/encoding/DeltaCodec.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import io.github.dfa1.vortex.core.ArrayStats;
66
import io.github.dfa1.vortex.core.DType;
77
import io.github.dfa1.vortex.core.PType;
8+
import io.github.dfa1.vortex.core.VortexException;
89

910
import java.lang.foreign.Arena;
1011
import java.lang.foreign.MemorySegment;
@@ -107,7 +108,7 @@ private static long[] toLongs(Object data, PType ptype) {
107108
yield r;
108109
}
109110
case I64, U64 -> (long[]) data;
110-
default -> throw new UnsupportedOperationException("unsupported ptype: " + ptype);
111+
default -> throw new VortexException(CodecId.FASTLANES_DELTA, "unsupported ptype: " + ptype);
111112
};
112113
}
113114

@@ -226,7 +227,7 @@ public EncodeResult encode(DType dtype, Object data) {
226227
public Array decode(DecodeContext ctx) {
227228
ByteBuffer rawMeta = ctx.metadata();
228229
if (rawMeta == null || rawMeta.capacity() < 17) {
229-
throw new IllegalStateException("fastlanes.delta: missing or truncated metadata");
230+
throw new VortexException(CodecId.FASTLANES_DELTA, "missing or truncated metadata");
230231
}
231232
ByteBuffer meta = rawMeta.duplicate().order(ByteOrder.LITTLE_ENDIAN);
232233
long baseValue = meta.getLong(0);

core/src/main/java/io/github/dfa1/vortex/encoding/DictCodec.java

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import io.github.dfa1.vortex.core.ArrayStats;
77
import io.github.dfa1.vortex.core.DType;
88
import io.github.dfa1.vortex.core.PType;
9+
import io.github.dfa1.vortex.core.VortexException;
910

1011
import java.lang.foreign.MemorySegment;
1112
import java.lang.foreign.ValueLayout;
@@ -128,7 +129,7 @@ private static void writeCode(ByteBuffer buf, PType codePType, int code) {
128129
case U8 -> buf.put((byte) code);
129130
case U16 -> buf.putShort((short) code);
130131
case U32 -> buf.putInt(code);
131-
default -> throw new IllegalStateException("unexpected code type: " + codePType);
132+
default -> throw new VortexException(CodecId.VORTEX_DICT, "unexpected code type: " + codePType);
132133
}
133134
}
134135

@@ -169,7 +170,7 @@ private static long readCode(MemorySegment buf, PType codePType, long i) {
169170
case U8 -> Byte.toUnsignedLong(buf.get(ValueLayout.JAVA_BYTE, i));
170171
case U16 -> Short.toUnsignedLong(buf.get(LE_SHORT, i * 2));
171172
case U32 -> Integer.toUnsignedLong(buf.get(LE_INT, i * 4));
172-
default -> throw new IllegalStateException("unexpected code type: " + codePType);
173+
default -> throw new VortexException(CodecId.VORTEX_DICT, "unexpected code type: " + codePType);
173174
};
174175
}
175176

@@ -232,7 +233,7 @@ public EncodeResult encode(DType dtype, Object data) {
232233
@Override
233234
public Array decode(DecodeContext ctx) {
234235
if (ctx.metadata() == null || !ctx.metadata().hasRemaining()) {
235-
throw new IllegalStateException("vortex.dict: missing metadata");
236+
throw new VortexException(CodecId.VORTEX_DICT, "missing metadata");
236237
}
237238

238239
ByteBuffer meta = ctx.metadata();
@@ -269,7 +270,7 @@ private Array decodeRustProto(DecodeContext ctx, byte[] metaBytes) {
269270
try {
270271
meta = EncodingProtos.DictMetadata.parseFrom(metaBytes);
271272
} catch (InvalidProtocolBufferException e) {
272-
throw new IllegalStateException("vortex.dict: invalid proto metadata", e);
273+
throw new VortexException(CodecId.VORTEX_DICT, "invalid proto metadata", e);
273274
}
274275

275276
PType codePType = PType.values()[meta.getCodesPtype().getNumber()];
@@ -293,7 +294,7 @@ private Array decodeRustProto(DecodeContext ctx, byte[] metaBytes) {
293294
case U8 -> expandU8(codesBuf, valuesBuf, out, rowCount, elemSize);
294295
case U16 -> expandU16(codesBuf, valuesBuf, out, rowCount, elemSize);
295296
case U32 -> expandU32(codesBuf, valuesBuf, out, rowCount, elemSize);
296-
default -> throw new IllegalStateException("unexpected code type: " + codePType);
297+
default -> throw new VortexException(CodecId.VORTEX_DICT, "unexpected code type: " + codePType);
297298
}
298299
return new Array(ctx.dtype(), rowCount, new MemorySegment[]{out.asReadOnly()}, Array.NO_CHILDREN, ArrayStats.empty());
299300
}

0 commit comments

Comments
 (0)