11# vortex-java
22
33[ ![ CI] ( https://github.com/dfa1/vortex-java/actions/workflows/ci.yml/badge.svg )] ( https://github.com/dfa1/vortex-java/actions )
4+ [ ![ License] ( https://img.shields.io/badge/License-Apache%202.0-blue.svg )] ( https://opensource.org/license/Apache-2.0 )
45
56> ** Alpha** — not production-ready. APIs will change without notice.
67
78Pure-Java reader/writer for the [ Vortex] ( https://github.com/spiraldb/vortex ) columnar file format.
89
9- Vortex is a shared open format with multiple independent implementations (Rust, Go, Java).
10- Files written by any implementation are readable by all others — no vendor lock-in, no
11- format translation at the boundary.
10+ From (https://vortex.dev/blog/btrblocks-compressor ):
11+ > We've written about individual compression codecs in Vortex before: FastLanes for bit-packing integers, FSST for strings, and ALP for floating point. But we've never explained how Vortex decides which codec to use for a given column, or how it layers multiple codecs on top of each other.
1212
13- ## Status
13+ > On TPC-H at scale factor 10, Vortex files are 38% smaller and decompress 10–25x faster than Parquet with ZSTD, without using any general-purpose compression. The difference comes down to how the codecs are selected and composed, a framework inspired by the BtrBlocks paper from TU Munich.
1414
15- - Pure-Java reader for primitive, sequence, ALP, dict, FSST (stable)
16- - Local (mmap) or Remote (HTTPS, single read of last 65K) (stable)
17- - Writer: in progress
18- - Benchmark vs Rust+JNI: Java beats JNI 1.5×–11.5× across read/write workloads (see Benchmarks)
19- - ** File size trade-off:** Java-written files are larger than Rust-written files (up to ~ 2×
20- with ` cascading(3) ` ). The Rust writer applies more compression passes; the Java writer
21- covers ALP, bitpacking, and FSST but not the full Rust encoding set yet. Files are still
22- significantly smaller than CSV. Cross-implementation verified by ` FileSizeComparisonIntegrationTest ` .
23- - Full encoding coverage: in progress
24- - Vectorized decode paths (Panama Vector API): planned
25- - Iceberg/Spark/Flink integration: not available yet
15+ 🎓 Maximilian Kuschewski, David Sauerwein, Adnan Alhomssi, and Viktor Leis. 2023. BtrBlocks: Efficient Columnar Compression for Data Lakes. Proc. ACM Manag. Data 1, 2, Article 118 (June 2023). > https://doi.org/10.1145/3589263
16+
17+ > The core idea: don't pick one codec. Try them all, and let the data decide.
2618
2719## Motivation
2820
21+ This is a third-party implementation with the idea that files written by any implementation
22+ are readable by all others — no vendor lock-in, no format translation at the boundary.
23+
24+ | Project | Language | Notes |
25+ | -------------------------------------------------------------| ----------| ---------------------------------------------------------------------|
26+ | [ spiraldb/vortex] ( https://github.com/spiraldb/vortex ) | Rust | Reference implementation + JNI bindings |
27+ | [ spiraldb/vortex-go] ( https://github.com/spiraldb/vortex-go ) | Go | Pure-language port
28+
2929The official Vortex ecosystem provides JVM bindings via JNI (bundled native ` .so ` /` .dylib ` ).
3030JNI bindings are fast but add deployment friction: platform-specific artifacts, native build
3131toolchains, and crash-domain coupling between the JVM and native code.
3232
3333This library takes a different approach — 100% Java, no JNI, no ` sun.misc.Unsafe ` .
34- It uses the Java FFM API (` MemorySegment ` / ` Arena ` , Java 25+) for zero-copy memory-mapped reads, making it easier to:
34+ It uses the Java FFM API (` MemorySegment ` / ` Arena ` , Java 25+) for zero-copy memory-mapped reads,
35+ making it easier to:
3536
3637- embed in any JVM project without native-library management
3738- build and test on any platform with a standard JDK
@@ -44,136 +45,6 @@ It uses the Java FFM API (`MemorySegment` / `Arena`, Java 25+) for zero-copy mem
4445- Anyone who wants mmap‑backed, zero‑copy columnar reads without first decompressing
4546 the whole file (or row chunk)
4647
47- ### Why fewer layers = faster
48-
49- ```
50- vortex-jni vortex-java
51- ────────────────────────────── ──────────────────────────
52- ┌──────────────────────────┐ ┌──────────────────────┐
53- │ Java App │ │ Java App │
54- │ (BigIntVector.get(i)) │ │ (buffer.getAtIndex) │
55- └────────────┬─────────────┘ └──────────┬───────────┘
56- │ Arrow Java API │ FFM API
57- ┌────────────▼─────────────┐ │ (MemorySegment,
58- │ Apache Arrow (Java) │ │ zero-copy slice)
59- │ VectorSchemaRoot, │ │
60- │ BigIntVector, … │ │
61- └────────────┬─────────────┘ ┌──────────▼───────────┐
62- │ Arrow C Data Interface │ OS mmap region │
63- │ (ArrowArray/ArrowSchema) │ (file on disk) │
64- │ + JNI boundary crossing └──────────────────────┘
65- ┌────────────▼─────────────┐
66- │ Native lib │
67- │ (.so / .dylib) │
68- │ Rust decode │
69- └────────────┬─────────────┘
70- │ mmap / read
71- ┌────────────▼─────────────┐
72- │ OS mmap region │
73- │ (file on disk) │
74- └──────────────────────────┘
75-
76- 4 layers, 1 JNI crossing, 2 layers, 0 boundary crossings,
77- Arrow C Data Interface overhead no intermediate format
78- ```
79-
80- The JNI path pays three costs per batch: (1) a JNI boundary crossing to call into native
81- code, (2) the Arrow C Data Interface handshake to pass decoded buffers back to the JVM as
82- ` ArrowArray ` /` ArrowSchema ` structs, and (3) materialising the result into Apache Arrow
83- ` VectorSchemaRoot ` objects before the application can read a single value. The JIT cannot
84- inline or optimise across the JNI boundary.
85-
86- ` vortex-java ` eliminates all of that. The FFM API (` MemorySegment ` ) gives Java code a
87- typed, bounds-checked view directly into the OS mmap region — the same physical memory the
88- file occupies. Decoding reads bytes directly from that view with no copies, no intermediate
89- Arrow format, and no boundary crossings. The JIT sees the full decode path as ordinary Java
90- bytecode.
91-
92- ## Benchmarks
93-
94- JMH throughput (ops/s = full-file scans per second). Higher is better.
95-
96- ** Environment:** Apple M5, OpenJDK 27-jep401ea3 (Valhalla EA), 3 warmup × 3 s, 5 measurement × 5 s, fork 1.
97-
98- ### OHLC read — 10 M rows, 58.9 MB (Rust-written file, single-column projection)
99-
100- | Benchmark | Java (ops/s) | JNI/Rust (ops/s) | Java speedup |
101- | ----------------| ------------------| ------------------| --------------|
102- | close (F64/ALP)| 76.7 ± 0.3 | 50.4 ± 2.8 | ** 1.5×** |
103- | volume (I64) | 127.9 ± 2.3 | 52.9 ± 0.6 | ** 2.4×** |
104- | symbol (varbin)| 110.4 ± 0.4 | 9.6 ± 0.9 | ** 11.5×** |
105-
106- ### OHLC write — 10 M rows
107-
108- | Benchmark | Java (ops/s) | JNI/Rust (ops/s) | Java speedup |
109- | -----------| --------------| ------------------| --------------|
110- | write | 4.4 ± 1.1 | 0.7 ± 0.1 | ** 6.4×** |
111-
112- ### Big-file scan — 100 M rows × 4 I64 columns, ~ 3 GB (Rust-written file, all columns)
113-
114- | Benchmark | Java (ops/s) | JNI/Rust (ops/s) | Java speedup |
115- | -----------| --------------| ------------------| --------------|
116- | scan | 20.4 ± 0.9 | 5.7 ± 0.6 | ** 3.6×** |
117-
118- ## Design principles
119-
120- - Zero-copy everywhere
121- - No JNI
122- - No Unsafe -- [ FFM vs Unsafe] ( https://inside.java/2025/06/12/ffm-vs-unsafe/ ) — Maurizio Cimadamore's deep-dive on why FFM (` MemorySegment ` /` Arena ` ) supersedes ` sun.misc.Unsafe ` : safety, performance, and the JVM's path forward
123- - Align with vortex-rust and Vortex-go semantics
124- - Make the JIT happy (constant layouts, predictable strides, no virtual dispatch in hot loops)
125- - Prepare for the Vector API / Valhalla
126- - Rigorous testing: unit tests + property-based testing + cross-language integration tests
127-
128- ### Testing strategy
129-
130- Unit tests verify internal correctness (encoding round-trips, edge cases), but the format has no
131- formal specification — the Rust implementation is the ground truth. Unit tests alone miss
132- cross-language wire-format bugs: Java can round-trip a value internally while writing bytes that
133- another implementation cannot decode.
134-
135- The ` integration ` module addresses this by using the Rust JNI reader as a ** test oracle** :
136- Java writes a file, the Rust reader decodes it, and the values are compared exactly.
137- [ Property-based testing] ( https://jqwik.net/ ) (jqwik) generates large, diverse inputs automatically,
138- covering edge cases no hand-written test would anticipate.
139-
140- This combination caught two real bugs in ALP floating-point encoding:
141- - Java selected exponents outside the range Rust's decoder accepts (silent data corruption)
142- - Java's encode round-trip check used a different floating-point associativity than Rust's decode
143- (` encoded * (F10[f] * IF10[e]) ` vs ` (encoded * F10[f]) * IF10[e] ` ), passing values that Rust
144- decoded differently
145-
146- Both bugs were invisible to pure-Java tests and would have shipped undetected without the
147- cross-language oracle.
148-
149- ## Implementations
150-
151- | Project | Language | Notes |
152- | -------------------------------------------------------------| ----------| ---------------------------------------------------------------------|
153- | [ spiraldb/vortex] ( https://github.com/spiraldb/vortex ) | Rust | Reference implementation + JNI bindings |
154- | [ spiraldb/vortex-go] ( https://github.com/spiraldb/vortex-go ) | Go | Pure-language port |
155- | [ dfa1/vortex-java] ( https://github.com/dfa1/vortex-java ) | Java | This project — FFM-based, no JNI, no Unsafe |
156-
157- All three implementations share the same binary format and can read each other's files.
158-
159-
160- ## Serialization formats
161-
162- The format uses two serialization libraries for different roles:
163-
164- | Format | Used for | Why |
165- | -----------------| --------------------------------------| ----------------------------------------------------------------------------------------|
166- | ** FlatBuffers** | Footer, Layout, Array structure | Zero-copy random access — fields read directly from memory-mapped bytes, no allocation |
167- | ** Protobuf** | Codec metadata, DType, Scalar values | Schema evolution and cross-language compatibility for small blobs |
168-
169- FlatBuffers suit the file-structure layer: the footer is parsed once at open and the layout tree is traversed during
170- scan — both benefit from direct field access on mapped memory. Protobuf suits codec metadata: tiny blobs parsed once per
171- chunk, where schema evolution matters more than zero-copy speed.
172-
173- Replacing protobuf with FlatBuffers is not viable — existing ` .vortex ` files produced by the Rust reference
174- implementation embed protobuf bytes in codec metadata blobs, and wire compatibility requires matching the format
175- exactly.
176-
17748## Quickstart
17849
17950Add the library to your build (example, Maven):
@@ -308,6 +179,9 @@ java -jar cli/target/vortex.jar import data/trades.csv
308179# writes data/trades.vortex, prints size savings
309180` ` `
310181
182+
183+ # Development
184+
311185# # Requirements
312186
313187- Java 25+
@@ -340,6 +214,123 @@ java -jar performance/target/benchmarks.jar RustVsJavaWriteBenchmark.javaWrite
340214java -jar performance/target/benchmarks.jar
341215` ` `
342216
343- # # License
217+ # # Design principles
218+
219+ - Zero-copy everywhere
220+ - No JNI
221+ - No Unsafe -- [FFM vs Unsafe](https://inside.java/2025/06/12/ffm-vs-unsafe/) — Maurizio Cimadamore' s deep-dive on why FFM (`MemorySegment`/`Arena`) supersedes `sun.misc.Unsafe`: safety, performance, and the JVM' s path forward
222+ - Align with vortex-rust and Vortex-go semantics
223+ - Make the JIT happy (constant layouts, predictable strides, no virtual dispatch in hot loops)
224+ - Prepare for the Vector API / Valhalla
225+ - Rigorous testing: unit tests + property-based testing + cross-language integration tests
226+ - Target Vector API as soon it is available https://openjdk.org/jeps/338
227+
228+ # ## Testing strategy
229+
230+ Unit tests verify internal correctness (encoding round-trips, edge cases), but the format has no
231+ formal specification — the Rust implementation is the ground truth. Unit tests alone miss
232+ cross-language wire-format bugs: Java can round-trip a value internally while writing bytes that
233+ another implementation cannot decode.
234+
235+ The ` integration` module addresses this by using the Rust JNI reader as a ** test oracle** :
236+ Java writes a file, the Rust reader decodes it, and the values are compared exactly.
237+ [Property-based testing](https://jqwik.net/) (jqwik) generates large, diverse inputs automatically,
238+ covering edge cases no hand-written test would anticipate.
239+
240+ This combination caught two real bugs in ALP floating-point encoding:
241+ - Java selected exponents outside the range Rust' s decoder accepts (silent data corruption)
242+ - Java' s encode round-trip check used a different floating-point associativity than Rust' s decode
243+ (`encoded * (F10[f] * IF10[e])` vs `(encoded * F10[f]) * IF10[e]`), passing values that Rust
244+ decoded differently
245+
246+ Both bugs were invisible to pure-Java tests and would have shipped undetected without the
247+ cross-language oracle.
248+
249+ ## Reference
250+ |
251+
252+ ## Benchmarks
253+
254+ JMH throughput (ops/s = full-file scans per second). Higher is better.
255+
256+ **Environment:** Apple M5, OpenJDK 25, 3 warmup × 3 s, 5 measurement × 5 s, fork 1.
257+
258+ ### OHLC read — 10 M rows, 58.9 MB (Rust-written file, single-column projection)
259+
260+ | Benchmark | Java (ops/s) | JNI/Rust (ops/s) | Java speedup |
261+ |----------------|------------------|------------------|--------------|
262+ | close (F64/ALP)| 76.7 ± 0.3 | 50.4 ± 2.8 | **1.5×** |
263+ | volume (I64) | 127.9 ± 2.3 | 52.9 ± 0.6 | **2.4×** |
264+ | symbol (varbin)| 110.4 ± 0.4 | 9.6 ± 0.9 | **11.5×** |
265+
266+ ### OHLC write — 10 M rows
267+
268+ | Benchmark | Java (ops/s) | JNI/Rust (ops/s) | Java speedup |
269+ |-----------|--------------|------------------|--------------|
270+ | write | 4.4 ± 1.1 | 0.7 ± 0.1 | **6.4×** |
271+
272+ * the Java part is faster but also produces bigger files (there much more work there)
273+
274+ ### Big-file scan — 100 M rows × 4 I64 columns, ~3 GB (Rust-written file, all columns)
275+
276+ | Benchmark | Java (ops/s) | JNI/Rust (ops/s) | Java speedup |
277+ |-----------|--------------|------------------|--------------|
278+ | scan | 20.4 ± 0.9 | 5.7 ± 0.6 | **3.6×** |
279+
280+ ### Why fewer layers = faster
281+
282+ This is my hypothesis:
283+ ```
284+ vortex-jni vortex-java
285+ ────────────────────────────── ──────────────────────────
286+ ┌──────────────────────────┐ ┌──────────────────────┐
287+ │ Java App │ │ Java App │
288+ │ (BigIntVector.get(i)) │ │ (buffer.getAtIndex) │
289+ └────────────┬─────────────┘ └──────────┬───────────┘
290+ │ Arrow Java API │ FFM API
291+ ┌────────────▼─────────────┐ │ (MemorySegment,
292+ │ Apache Arrow (Java) │ │ zero-copy slice)
293+ │ VectorSchemaRoot, │ │
294+ │ BigIntVector, … │ │
295+ └────────────┬─────────────┘ ┌──────────▼───────────┐
296+ │ Arrow C Data Interface │ OS mmap region │
297+ │ (ArrowArray/ArrowSchema) │ (file on disk) │
298+ │ + JNI boundary crossing └──────────────────────┘
299+ ┌────────────▼─────────────┐
300+ │ Native lib │
301+ │ (.so / .dylib) │
302+ │ Rust decode │
303+ └────────────┬─────────────┘
304+ │ mmap / read
305+ ┌────────────▼─────────────┐
306+ │ OS mmap region │
307+ │ (file on disk) │
308+ └──────────────────────────┘
309+
310+ 4 layers, 1 JNI crossing, 2 layers, 0 boundary crossings,
311+ Arrow C Data Interface overhead no intermediate format
312+ ```
313+
314+ The JNI path pays three costs per batch: (1) a JNI boundary crossing to call into native
315+ code, (2) the Arrow C Data Interface handshake to pass decoded buffers back to the JVM as
316+ `ArrowArray`/`ArrowSchema` structs, and (3) materialising the result into Apache Arrow
317+ `VectorSchemaRoot` objects before the application can read a single value. The JIT cannot
318+ inline or optimise across the JNI boundary.
319+
320+ `vortex-java` eliminates all of that. The FFM API (`MemorySegment`) gives Java code a
321+ typed, bounds-checked view directly into the OS mmap region — the same physical memory the
322+ file occupies. Decoding reads bytes directly from that view with no copies, no intermediate
323+ Arrow format, and no boundary crossings. The JIT sees the full decode path as ordinary Java
324+ bytecode.
325+
326+ ## Contributing
327+
328+ Forks and contributions are welcome! Please feel free to fork the repository and open a pull request.
329+ When submitting a PR, include tests and update documentation where applicable
330+ (follow guidelines in CLAUDE.md).
331+
332+ ### AI-assisted development
344333
345- Apache 2.0
334+ This project uses [Claude Code](https://claude.ai/code) heavily for implementation
335+ work — generating mapping, test generation and documentation.
336+ **Architecture, API design, and all decisions are human-driven**.
0 commit comments