|
1 | 1 | package io.github.dfa1.vortex.integration; |
2 | 2 |
|
| 3 | +import dev.vortex.api.DataSource; |
| 4 | +import dev.vortex.api.Expression; |
| 5 | +import dev.vortex.api.Partition; |
| 6 | +import dev.vortex.api.Scan; |
| 7 | +import dev.vortex.api.ScanOptions; |
3 | 8 | import dev.vortex.api.Session; |
4 | 9 | import dev.vortex.api.VortexWriter; |
5 | 10 | import dev.vortex.arrow.ArrowAllocation; |
6 | 11 | import dev.vortex.jni.NativeLoader; |
| 12 | +import io.github.dfa1.vortex.core.DType; |
| 13 | +import io.github.dfa1.vortex.core.PType; |
| 14 | +import io.github.dfa1.vortex.core.array.LongArray; |
7 | 15 | import io.github.dfa1.vortex.encoding.EncodingRegistry; |
8 | 16 | import io.github.dfa1.vortex.io.VortexReader; |
9 | 17 | import io.github.dfa1.vortex.scan.ScanResult; |
|
14 | 22 | import org.apache.arrow.vector.BigIntVector; |
15 | 23 | import org.apache.arrow.vector.Float8Vector; |
16 | 24 | import org.apache.arrow.vector.VectorSchemaRoot; |
| 25 | +import org.apache.arrow.vector.ipc.ArrowReader; |
17 | 26 | import org.apache.arrow.vector.types.FloatingPointPrecision; |
18 | 27 | import org.apache.arrow.vector.types.pojo.ArrowType; |
19 | 28 | import org.apache.arrow.vector.types.pojo.Field; |
20 | 29 | import org.apache.arrow.vector.types.pojo.Schema; |
| 30 | +import org.junit.jupiter.api.Disabled; |
21 | 31 | import org.junit.jupiter.api.Test; |
22 | 32 | import org.junit.jupiter.api.io.TempDir; |
23 | 33 |
|
24 | 34 | import java.io.IOException; |
25 | 35 | import java.lang.foreign.ValueLayout; |
| 36 | +import java.net.URI; |
26 | 37 | import java.nio.ByteOrder; |
| 38 | +import java.nio.file.Files; |
27 | 39 | import java.nio.file.Path; |
| 40 | +import java.nio.file.StandardCopyOption; |
28 | 41 | import java.util.ArrayList; |
| 42 | +import java.util.Arrays; |
29 | 43 | import java.util.HashMap; |
30 | 44 | import java.util.List; |
31 | 45 |
|
32 | 46 | import static org.assertj.core.api.Assertions.assertThat; |
| 47 | +import static org.junit.jupiter.api.Assumptions.assumeTrue; |
33 | 48 |
|
34 | 49 | /// Cross-compatibility: Rust (JNI) writer → Java reader. |
35 | 50 | class RustWritesJavaReadsIntegrationTest { |
36 | 51 |
|
| 52 | + private static final String S3_BASE = "https://vortex-compat-fixtures.s3.amazonaws.com/v0.72.0/arrays/"; |
| 53 | + |
37 | 54 | private static final Session SESSION = Session.create(); |
38 | 55 | private static final BufferAllocator ALLOCATOR = ArrowAllocation.rootAllocator(); |
39 | 56 | private static final Schema JNI_SCHEMA = new Schema(List.of( |
@@ -197,4 +214,148 @@ void jniWriter_javaReader_fewUniqueF64Values(@TempDir Path tmp) throws IOExcepti |
197 | 214 | assertThat(sum).isCloseTo(21_998.9, org.assertj.core.data.Offset.offset(0.1)); |
198 | 215 | } |
199 | 216 | } |
| 217 | + |
| 218 | + // ── S3 fixture round-trip: Rust-written pco → Java reader ──────────────── |
| 219 | + |
| 220 | + @Disabled("pco.vortex has IntMult columns (mode=1) — blocked on Phase 8") |
| 221 | + @Test |
| 222 | + void s3_pcoVortex_javaDecodeMatchesJni(@TempDir Path tmp) throws Exception { |
| 223 | + // Given — pco.vortex: synthetic file with all pco ptypes; Classic+Consecutive mode |
| 224 | + assumeNetworkAvailable(); |
| 225 | + Path file = downloadIfMissing(tmp, "pco.vortex"); |
| 226 | + String col = firstI64Column(file); |
| 227 | + |
| 228 | + // When |
| 229 | + long[] jni = readJniLongColumn(file, col); |
| 230 | + long[] java = readJavaLongColumn(file, col); |
| 231 | + |
| 232 | + // Then — same values (order may differ across chunks) |
| 233 | + Arrays.sort(jni); |
| 234 | + Arrays.sort(java); |
| 235 | + assertThat(java).containsExactly(jni); |
| 236 | + } |
| 237 | + |
| 238 | + @Test |
| 239 | + void s3_tpchLineitem_javaDecodeMatchesJni(@TempDir Path tmp) throws Exception { |
| 240 | + // Given — tpch_lineitem.compact.vortex: I64/I32/Decimal/date columns, Classic+Consecutive |
| 241 | + assumeNetworkAvailable(); |
| 242 | + Path file = downloadIfMissing(tmp, "tpch_lineitem.compact.vortex"); |
| 243 | + String col = firstI64Column(file); |
| 244 | + |
| 245 | + // When |
| 246 | + long[] jni = readJniLongColumn(file, col); |
| 247 | + long[] java = readJavaLongColumn(file, col); |
| 248 | + |
| 249 | + // Then |
| 250 | + Arrays.sort(jni); |
| 251 | + Arrays.sort(java); |
| 252 | + assertThat(java).containsExactly(jni); |
| 253 | + } |
| 254 | + |
| 255 | + @Test |
| 256 | + void s3_tpchOrders_javaDecodeMatchesJni(@TempDir Path tmp) throws Exception { |
| 257 | + // Given — tpch_orders.compact.vortex: I64/I32/Decimal/date columns, Classic+Consecutive |
| 258 | + assumeNetworkAvailable(); |
| 259 | + Path file = downloadIfMissing(tmp, "tpch_orders.compact.vortex"); |
| 260 | + String col = firstI64Column(file); |
| 261 | + |
| 262 | + // When |
| 263 | + long[] jni = readJniLongColumn(file, col); |
| 264 | + long[] java = readJavaLongColumn(file, col); |
| 265 | + |
| 266 | + // Then |
| 267 | + Arrays.sort(jni); |
| 268 | + Arrays.sort(java); |
| 269 | + assertThat(java).containsExactly(jni); |
| 270 | + } |
| 271 | + |
| 272 | + @Test |
| 273 | + void s3_clickbenchHits5k_javaDecodeMatchesJni(@TempDir Path tmp) throws Exception { |
| 274 | + // Given — clickbench_hits_5k.compact.vortex: I16/I32/I64/ts-I64 columns, Classic+Consecutive |
| 275 | + assumeNetworkAvailable(); |
| 276 | + Path file = downloadIfMissing(tmp, "clickbench_hits_5k.compact.vortex"); |
| 277 | + String col = firstI64Column(file); |
| 278 | + |
| 279 | + // When |
| 280 | + long[] jni = readJniLongColumn(file, col); |
| 281 | + long[] java = readJavaLongColumn(file, col); |
| 282 | + |
| 283 | + // Then |
| 284 | + Arrays.sort(jni); |
| 285 | + Arrays.sort(java); |
| 286 | + assertThat(java).containsExactly(jni); |
| 287 | + } |
| 288 | + |
| 289 | + // ── S3 helpers ──────────────────────────────────────────────────────────── |
| 290 | + |
| 291 | + private static String firstI64Column(Path file) throws IOException { |
| 292 | + try (var vf = VortexReader.open(file, EncodingRegistry.empty())) { |
| 293 | + if (vf.dtype() instanceof DType.Struct struct) { |
| 294 | + for (int i = 0; i < struct.fieldNames().size(); i++) { |
| 295 | + if (struct.fieldTypes().get(i) instanceof DType.Primitive(PType pt, boolean _) && pt == PType.I64) { |
| 296 | + return struct.fieldNames().get(i); |
| 297 | + } |
| 298 | + } |
| 299 | + } |
| 300 | + throw new AssertionError("no I64 column found in " + file.getFileName()); |
| 301 | + } |
| 302 | + } |
| 303 | + |
| 304 | + private static long[] readJniLongColumn(Path file, String column) throws IOException { |
| 305 | + String uri = file.toAbsolutePath().toUri().toString(); |
| 306 | + ScanOptions opts = ScanOptions.builder() |
| 307 | + .projection(Expression.select(new String[]{column}, Expression.root())) |
| 308 | + .build(); |
| 309 | + var longs = new ArrayList<Long>(); |
| 310 | + DataSource ds = DataSource.open(SESSION, uri); |
| 311 | + Scan scan = ds.scan(opts); |
| 312 | + while (scan.hasNext()) { |
| 313 | + Partition partition = scan.next(); |
| 314 | + try (ArrowReader reader = partition.scanArrow(ALLOCATOR)) { |
| 315 | + while (reader.loadNextBatch()) { |
| 316 | + VectorSchemaRoot root = reader.getVectorSchemaRoot(); |
| 317 | + BigIntVector vec = (BigIntVector) root.getVector(column); |
| 318 | + for (int i = 0; i < root.getRowCount(); i++) { |
| 319 | + longs.add(vec.get(i)); |
| 320 | + } |
| 321 | + } |
| 322 | + } |
| 323 | + } |
| 324 | + return longs.stream().mapToLong(Long::longValue).toArray(); |
| 325 | + } |
| 326 | + |
| 327 | + private static long[] readJavaLongColumn(Path file, String column) throws IOException { |
| 328 | + try (var vf = VortexReader.open(file, EncodingRegistry.loadAll())) { |
| 329 | + var longs = new ArrayList<Long>(); |
| 330 | + var iter = vf.scan(io.github.dfa1.vortex.scan.ScanOptions.columns(column)); |
| 331 | + while (iter.hasNext()) { |
| 332 | + ScanResult r = iter.next(); |
| 333 | + LongArray arr = (LongArray) r.columns().get(column); |
| 334 | + for (long i = 0; i < arr.length(); i++) { |
| 335 | + longs.add(arr.getLong(i)); |
| 336 | + } |
| 337 | + } |
| 338 | + return longs.stream().mapToLong(Long::longValue).toArray(); |
| 339 | + } |
| 340 | + } |
| 341 | + |
| 342 | + private static Path downloadIfMissing(Path tmp, String name) throws Exception { |
| 343 | + Path cached = Path.of("/tmp/pco-fixtures", name); |
| 344 | + if (Files.exists(cached)) { |
| 345 | + return cached; |
| 346 | + } |
| 347 | + Path dest = tmp.resolve(name); |
| 348 | + try (var in = URI.create(S3_BASE + name).toURL().openStream()) { |
| 349 | + Files.copy(in, dest, StandardCopyOption.REPLACE_EXISTING); |
| 350 | + } |
| 351 | + return dest; |
| 352 | + } |
| 353 | + |
| 354 | + private static void assumeNetworkAvailable() { |
| 355 | + try { |
| 356 | + URI.create("https://vortex-compat-fixtures.s3.amazonaws.com").toURL().openStream().close(); |
| 357 | + } catch (Exception e) { |
| 358 | + assumeTrue(false, "no network"); |
| 359 | + } |
| 360 | + } |
200 | 361 | } |
0 commit comments