|
10 | 10 | import dev.vortex.jni.NativeLoader; |
11 | 11 | import io.github.dfa1.vortex.core.DType; |
12 | 12 | import io.github.dfa1.vortex.core.PType; |
| 13 | +import io.github.dfa1.vortex.encoding.VarBinEncoding; |
13 | 14 | import io.github.dfa1.vortex.writer.VortexWriter; |
14 | 15 | import io.github.dfa1.vortex.writer.WriteOptions; |
15 | 16 | import org.apache.arrow.memory.BufferAllocator; |
16 | 17 | import org.apache.arrow.vector.BigIntVector; |
17 | 18 | import org.apache.arrow.vector.Float8Vector; |
| 19 | +import org.apache.arrow.vector.IntVector; |
| 20 | +import org.apache.arrow.vector.VarCharVector; |
18 | 21 | import org.apache.arrow.vector.VectorSchemaRoot; |
19 | 22 | import org.apache.arrow.vector.ipc.ArrowReader; |
20 | 23 | import org.junit.jupiter.api.Test; |
@@ -42,6 +45,21 @@ class JavaWritesRustReadsIntegrationTest { |
42 | 45 | new DType.Primitive(PType.F64, false)), |
43 | 46 | false); |
44 | 47 |
|
| 48 | + private static final DType.Struct I32_SCHEMA = new DType.Struct( |
| 49 | + List.of("v"), |
| 50 | + List.of(new DType.Primitive(PType.I32, false)), |
| 51 | + false); |
| 52 | + |
| 53 | + private static final DType.Struct STRING_SCHEMA = new DType.Struct( |
| 54 | + List.of("s"), |
| 55 | + List.of(new DType.Utf8(false)), |
| 56 | + false); |
| 57 | + |
| 58 | + private static final DType.Struct TS_SCHEMA = new DType.Struct( |
| 59 | + List.of("ts"), |
| 60 | + List.of(new DType.Primitive(PType.I64, false)), |
| 61 | + false); |
| 62 | + |
45 | 63 | private static final DType.Struct OHLC_SCHEMA = new DType.Struct( |
46 | 64 | List.of("date", "symbol", "open", "high", "low", "close", "volume"), |
47 | 65 | List.of( |
@@ -104,6 +122,52 @@ private static double[] readDoubleColumn(Path file, String column) throws IOExce |
104 | 122 | return doubles.stream().mapToDouble(Double::doubleValue).toArray(); |
105 | 123 | } |
106 | 124 |
|
| 125 | + private static int[] readIntColumn(Path file, String column) throws IOException { |
| 126 | + String uri = file.toAbsolutePath().toUri().toString(); |
| 127 | + ScanOptions opts = ScanOptions.builder() |
| 128 | + .projection(Expression.select(new String[]{column}, Expression.root())) |
| 129 | + .build(); |
| 130 | + var ints = new ArrayList<Integer>(); |
| 131 | + DataSource ds = DataSource.open(SESSION, uri); |
| 132 | + Scan scan = ds.scan(opts); |
| 133 | + while (scan.hasNext()) { |
| 134 | + Partition partition = scan.next(); |
| 135 | + try (ArrowReader reader = partition.scanArrow(ALLOCATOR)) { |
| 136 | + while (reader.loadNextBatch()) { |
| 137 | + VectorSchemaRoot root = reader.getVectorSchemaRoot(); |
| 138 | + IntVector vec = (IntVector) root.getVector(column); |
| 139 | + for (int i = 0; i < root.getRowCount(); i++) { |
| 140 | + ints.add(vec.get(i)); |
| 141 | + } |
| 142 | + } |
| 143 | + } |
| 144 | + } |
| 145 | + return ints.stream().mapToInt(Integer::intValue).toArray(); |
| 146 | + } |
| 147 | + |
| 148 | + private static String[] readStringColumn(Path file, String column) throws IOException { |
| 149 | + String uri = file.toAbsolutePath().toUri().toString(); |
| 150 | + ScanOptions opts = ScanOptions.builder() |
| 151 | + .projection(Expression.select(new String[]{column}, Expression.root())) |
| 152 | + .build(); |
| 153 | + var strings = new ArrayList<String>(); |
| 154 | + DataSource ds = DataSource.open(SESSION, uri); |
| 155 | + Scan scan = ds.scan(opts); |
| 156 | + while (scan.hasNext()) { |
| 157 | + Partition partition = scan.next(); |
| 158 | + try (ArrowReader reader = partition.scanArrow(ALLOCATOR)) { |
| 159 | + while (reader.loadNextBatch()) { |
| 160 | + VectorSchemaRoot root = reader.getVectorSchemaRoot(); |
| 161 | + VarCharVector vec = (VarCharVector) root.getVector(column); |
| 162 | + for (int i = 0; i < root.getRowCount(); i++) { |
| 163 | + strings.add(vec.getObject(i).toString()); |
| 164 | + } |
| 165 | + } |
| 166 | + } |
| 167 | + } |
| 168 | + return strings.toArray(String[]::new); |
| 169 | + } |
| 170 | + |
107 | 171 | // ── JNI read helpers ────────────────────────────────────────────────────── |
108 | 172 |
|
109 | 173 | @Test |
@@ -174,4 +238,103 @@ void javaWriter_jniReader_multipleChunks(@TempDir Path tmp) throws IOException { |
174 | 238 | // Then — JNI may merge chunks; verify all values present regardless of partition count |
175 | 239 | assertThat(decodedIds).containsExactly(1L, 2L, 3L, 4L, 5L); |
176 | 240 | } |
| 241 | + |
| 242 | + @Test |
| 243 | + void javaWriter_jniReader_i32Column(@TempDir Path tmp) throws IOException { |
| 244 | + // Given |
| 245 | + Path file = tmp.resolve("java_i32.vtx"); |
| 246 | + int[] data = {-100, 0, 1, 127, Integer.MAX_VALUE, Integer.MIN_VALUE}; |
| 247 | + try (var ch = FileChannel.open(file, StandardOpenOption.CREATE, StandardOpenOption.WRITE); |
| 248 | + var sut = VortexWriter.create(ch, I32_SCHEMA, WriteOptions.defaults())) { |
| 249 | + // When |
| 250 | + sut.writeChunk(Map.of("v", data)); |
| 251 | + } |
| 252 | + |
| 253 | + // Then |
| 254 | + int[] decoded = readIntColumn(file, "v"); |
| 255 | + assertThat(decoded).containsExactly(data); |
| 256 | + } |
| 257 | + |
| 258 | + @Test |
| 259 | + void javaWriter_jniReader_utf8Column(@TempDir Path tmp) throws IOException { |
| 260 | + // Given — force VarBinEncoding; DictEncoding.encodeUtf8 writes non-protobuf metadata |
| 261 | + // that the Rust reader rejects (known issue, tracked in DictEncoding) |
| 262 | + Path file = tmp.resolve("java_utf8.vtx"); |
| 263 | + String[] data = {"apple", "banana", "cherry", "date", "elderberry"}; |
| 264 | + try (var ch = FileChannel.open(file, StandardOpenOption.CREATE, StandardOpenOption.WRITE); |
| 265 | + var sut = VortexWriter.create(ch, STRING_SCHEMA, WriteOptions.defaults(), |
| 266 | + List.of(new VarBinEncoding()))) { |
| 267 | + // When |
| 268 | + sut.writeChunk(Map.of("s", data)); |
| 269 | + } |
| 270 | + |
| 271 | + // Then |
| 272 | + String[] decoded = readStringColumn(file, "s"); |
| 273 | + assertThat(decoded).containsExactly(data); |
| 274 | + } |
| 275 | + |
| 276 | + @Test |
| 277 | + void javaWriter_jniReader_largeChunk_twoFastLanesBlocks(@TempDir Path tmp) throws IOException { |
| 278 | + // Given — 2048 rows = exactly 2 full 1024-element FastLanes blocks |
| 279 | + Path file = tmp.resolve("java_large.vtx"); |
| 280 | + int n = 2048; |
| 281 | + long[] ids = new long[n]; |
| 282 | + double[] vals = new double[n]; |
| 283 | + for (int i = 0; i < n; i++) { |
| 284 | + ids[i] = i; |
| 285 | + vals[i] = i * 0.5; |
| 286 | + } |
| 287 | + try (var ch = FileChannel.open(file, StandardOpenOption.CREATE, StandardOpenOption.WRITE); |
| 288 | + var sut = VortexWriter.create(ch, SCHEMA, WriteOptions.defaults())) { |
| 289 | + // When |
| 290 | + sut.writeChunk(Map.of("id", ids, "value", vals)); |
| 291 | + } |
| 292 | + |
| 293 | + // Then |
| 294 | + long[] decodedIds = readLongColumn(file, "id"); |
| 295 | + assertThat(decodedIds).containsExactly(ids); |
| 296 | + } |
| 297 | + |
| 298 | + @Test |
| 299 | + void javaWriter_jniReader_monotonic_i64_cascading(@TempDir Path tmp) throws IOException { |
| 300 | + // Given — monotonic timestamps: FOR reduces to constant delta, bitpacked to ~10 bits |
| 301 | + Path file = tmp.resolve("java_ts.vtx"); |
| 302 | + int n = 5_000; |
| 303 | + long[] ts = new long[n]; |
| 304 | + for (int i = 0; i < n; i++) { |
| 305 | + ts[i] = 1_700_000_000L + (long) i * 1_000; |
| 306 | + } |
| 307 | + try (var ch = FileChannel.open(file, StandardOpenOption.CREATE, StandardOpenOption.WRITE); |
| 308 | + var sut = VortexWriter.create(ch, TS_SCHEMA, WriteOptions.cascading(3))) { |
| 309 | + // When |
| 310 | + sut.writeChunk(Map.of("ts", ts)); |
| 311 | + } |
| 312 | + |
| 313 | + // Then |
| 314 | + long[] decoded = readLongColumn(file, "ts"); |
| 315 | + assertThat(decoded).containsExactly(ts); |
| 316 | + } |
| 317 | + |
| 318 | + @Test |
| 319 | + void javaWriter_jniReader_cascading_ohlc_columnProjection(@TempDir Path tmp) throws IOException { |
| 320 | + // Given |
| 321 | + Path file = tmp.resolve("java_cascade_proj.vtx"); |
| 322 | + List<OhlcGenerator.OhlcBatch> batches = OhlcGenerator.generate(2_000, 1_000); |
| 323 | + try (var ch = FileChannel.open(file, StandardOpenOption.CREATE, StandardOpenOption.WRITE); |
| 324 | + var sut = VortexWriter.create(ch, OHLC_SCHEMA, WriteOptions.cascading(3))) { |
| 325 | + for (OhlcGenerator.OhlcBatch b : batches) { |
| 326 | + sut.writeChunk(Map.of( |
| 327 | + "date", b.dates(), "symbol", b.symbols(), |
| 328 | + "open", b.open(), "high", b.high(), |
| 329 | + "low", b.low(), "close", b.close(), "volume", b.volume())); |
| 330 | + } |
| 331 | + } |
| 332 | + |
| 333 | + // When — project only volume |
| 334 | + long[] volumes = readLongColumn(file, "volume"); |
| 335 | + |
| 336 | + // Then |
| 337 | + long[] expected = batches.stream().flatMapToLong(b -> Arrays.stream(b.volume())).toArray(); |
| 338 | + assertThat(volumes).containsExactlyInAnyOrder(expected); |
| 339 | + } |
177 | 340 | } |
0 commit comments