Skip to content

Commit ae9f95c

Browse files
dfa1claude
andcommitted
fix(reader,writer): actionable error when zstd binding is absent
The io.github.dfa1.zstd binding is optional; a file using vortex.zstd read without it surfaced as NoClassDefFoundError from the first binding call. Both codecs now probe availability once (Class.forName, no Error catching) and fail with a VortexException naming the two artifacts to add. Verified end-to-end: a real vortex.zstd file written with the binding reads fine with it and produces the actionable message without it; default write/read paths never touch the binding (probed with the jars absent). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent dc5687e commit ae9f95c

4 files changed

Lines changed: 46 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ claim measured against the Rust (JNI) oracle; behavior divergences documented in
3131

3232
### Changed
3333

34+
- The zstd binding stays a two-artifact opt-in (`io.github.dfa1.zstd:zstd` + `zstd-platform`, both `optional`; no JNI anywhere), and touching `vortex.zstd` without it now fails with an actionable `VortexException` naming the two artifacts to add, instead of a raw `NoClassDefFoundError`. Default write/read paths never touch the binding (measured). ([pending-zstd](https://github.com/dfa1/vortex-java/commits/main))
3435
- The little-endian `ValueLayout` constants moved from `PTypeIO.LE_*` to `VortexFormat.LE_*` — endianness is a property of the wire format, not of ptypes, and `VortexFormat` is where format facts live. Six classes that carried private copies (including reversed-name duplicates like `SHORT_LE`) now share the single source; nothing outside `VortexFormat` defines a `withOrder(LITTLE_ENDIAN)` layout. ([c060d34f](https://github.com/dfa1/vortex-java/commit/c060d34f))
3536
- `Chunk.columns()` returns an order-preserving `SequencedMap<ColumnName, Chunk.Column>` — one map instead of two parallel string-keyed maps, with each column's `Array` and `DType` traveling together in the `Column` carrier. `column(String)` stays as boundary sugar (plus a `column(ColumnName)` overload); iteration order is the schema/projection order, now guaranteed even for 1–2 column chunks. Typed names originate in `ScanIterator` from the file's already-certified schema. ([f8ad15d1](https://github.com/dfa1/vortex-java/commit/f8ad15d1))
3637

docs/compatibility.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@ A consumer that only needs to read Vortex files can depend on a strict subset:
2323
</dependency>
2424
```
2525

26+
The zstd binding is optional on both sides: files using `vortex.zstd` need
27+
`io.github.dfa1.zstd:zstd` plus `io.github.dfa1.zstd:zstd-platform` on the classpath (versions
28+
pinned by the vortex BOM); without them, touching a `vortex.zstd` segment fails with a
29+
`VortexException` that names the two artifacts. All other encodings are pure Java.
30+
2631
`./mvnw -pl core,reader,inspector verify` builds the read-only artifact set
2732
without the writer module on the classpath. `ServiceLoader<EncodingDecoder>`
2833
resolves only the standalone decoders in `reader`; no encoder class is loaded.

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

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,21 @@
2929
/// Read-only decoder for `vortex.zstd`.
3030
public final class ZstdEncodingDecoder implements EncodingDecoder {
3131

32+
// The io.github.dfa1.zstd binding is an OPTIONAL dependency: probe once so a reader
33+
// touching vortex.zstd without it gets an actionable message instead of a raw
34+
// NoClassDefFoundError surfacing from the first binding call.
35+
private static final boolean ZSTD_BINDING_PRESENT = zstdBindingPresent();
36+
37+
private static boolean zstdBindingPresent() {
38+
try {
39+
Class.forName("io.github.dfa1.zstd.ZstdDecompressContext", false,
40+
ZstdEncodingDecoder.class.getClassLoader());
41+
return true;
42+
} catch (ClassNotFoundException e) {
43+
return false;
44+
}
45+
}
46+
3247
/// Public no-arg constructor required by [java.util.ServiceLoader].
3348
public ZstdEncodingDecoder() {
3449
}
@@ -40,6 +55,11 @@ public EncodingId encodingId() {
4055

4156
@Override
4257
public Array decode(DecodeContext ctx) {
58+
if (!ZSTD_BINDING_PRESENT) {
59+
throw new VortexException(EncodingId.VORTEX_ZSTD, "this file uses vortex.zstd, but the "
60+
+ "optional zstd binding is not on the classpath — add io.github.dfa1.zstd:zstd "
61+
+ "and io.github.dfa1.zstd:zstd-platform (versions pinned by the vortex BOM)");
62+
}
4363
MemorySegment rawMeta = ctx.metadata();
4464
if (rawMeta == null) {
4565
throw new VortexException(EncodingId.VORTEX_ZSTD, "missing metadata");

writer/src/main/java/io/github/dfa1/vortex/writer/encode/ZstdEncodingEncoder.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,21 @@
2626
/// frames let a slice scan decompress only the frames overlapping its row range.
2727
public final class ZstdEncodingEncoder implements EncodingEncoder {
2828

29+
// The io.github.dfa1.zstd binding is an OPTIONAL dependency: probe once so a writer
30+
// touching vortex.zstd without it gets an actionable message instead of a raw
31+
// NoClassDefFoundError surfacing from the first binding call.
32+
private static final boolean ZSTD_BINDING_PRESENT = zstdBindingPresent();
33+
34+
private static boolean zstdBindingPresent() {
35+
try {
36+
Class.forName("io.github.dfa1.zstd.ZstdCompressContext", false,
37+
ZstdEncodingEncoder.class.getClassLoader());
38+
return true;
39+
} catch (ClassNotFoundException e) {
40+
return false;
41+
}
42+
}
43+
2944
/// Values per zstd frame; `0` (or any non-positive value) means a single frame for the whole array.
3045
private final long valuesPerFrame;
3146

@@ -62,6 +77,11 @@ public boolean acceptsNullable(DType dtype) {
6277

6378
@Override
6479
public EncodeResult encode(DType dtype, Object data, EncodeContext ctx) {
80+
if (!ZSTD_BINDING_PRESENT) {
81+
throw new VortexException(EncodingId.VORTEX_ZSTD, "vortex.zstd encoding requires the "
82+
+ "optional zstd binding — add io.github.dfa1.zstd:zstd and "
83+
+ "io.github.dfa1.zstd:zstd-platform (versions pinned by the vortex BOM)");
84+
}
6585
if (data instanceof NullableData nd) {
6686
if (dtype instanceof DType.Primitive dt) {
6787
return encodeNullablePrimitive(dt, nd, ctx);

0 commit comments

Comments
 (0)