Skip to content

Commit 1a81099

Browse files
authored
feat(arrow): expose Arrow IPC reader via registerArrow and readArrow (#52)
1 parent dd5d6fc commit 1a81099

8 files changed

Lines changed: 522 additions & 0 deletions

File tree

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
package org.apache.datafusion;
21+
22+
import org.apache.arrow.vector.types.pojo.Schema;
23+
import org.apache.datafusion.protobuf.ArrowReadOptionsProto;
24+
25+
/**
26+
* Configuration knobs for Arrow IPC sources passed to {@link SessionContext#registerArrow(String,
27+
* String, ArrowReadOptions)} and {@link SessionContext#readArrow(String, ArrowReadOptions)}.
28+
*
29+
* <p>Mirrors the subset of DataFusion's {@code ArrowReadOptions} that maps onto the Java surface
30+
* today: {@code fileExtension} (default {@code ".arrow"}) and an explicit Arrow {@code schema} that
31+
* bypasses on-read schema inference. {@code tablePartitionCols} is intentionally deferred --
32+
* neither Parquet nor CSV expose Hive-style partitioning on the Java side yet.
33+
*
34+
* <p>Arrow IPC files carry their own body compression (LZ4_FRAME / ZSTD per-buffer) inside the file
35+
* format itself, so unlike CSV / NDJSON there is no {@code FileCompressionType} setter.
36+
*/
37+
public final class ArrowReadOptions {
38+
39+
private String fileExtension = ".arrow";
40+
private Schema schema;
41+
42+
public ArrowReadOptions fileExtension(String ext) {
43+
this.fileExtension = ext;
44+
return this;
45+
}
46+
47+
public ArrowReadOptions schema(Schema schema) {
48+
this.schema = schema;
49+
return this;
50+
}
51+
52+
byte[] toBytes() {
53+
return ArrowReadOptionsProto.newBuilder().setFileExtension(fileExtension).build().toByteArray();
54+
}
55+
56+
Schema schema() {
57+
return schema;
58+
}
59+
}

core/src/main/java/org/apache/datafusion/SessionContext.java

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,70 @@ public DataFrame readParquet(String path, ParquetReadOptions options) {
298298
return new DataFrame(dfHandle);
299299
}
300300

301+
/** Register an Arrow IPC file (or directory of Arrow IPC files) as a table. */
302+
public void registerArrow(String name, String path) {
303+
registerArrow(name, path, new ArrowReadOptions());
304+
}
305+
306+
/**
307+
* Register an Arrow IPC file (or directory of Arrow IPC files) as a table with the supplied
308+
* {@link ArrowReadOptions}.
309+
*
310+
* @throws IllegalArgumentException if any of {@code name}, {@code path}, or {@code options} is
311+
* {@code null}.
312+
* @throws RuntimeException if registration fails (path not found, schema mismatch, etc.).
313+
*/
314+
public void registerArrow(String name, String path, ArrowReadOptions options) {
315+
if (nativeHandle == 0) {
316+
throw new IllegalStateException("SessionContext is closed");
317+
}
318+
if (name == null) {
319+
throw new IllegalArgumentException("registerArrow name must be non-null");
320+
}
321+
if (path == null) {
322+
throw new IllegalArgumentException("registerArrow path must be non-null");
323+
}
324+
if (options == null) {
325+
throw new IllegalArgumentException("registerArrow options must be non-null");
326+
}
327+
registerArrowWithOptions(
328+
nativeHandle,
329+
name,
330+
path,
331+
options.toBytes(),
332+
options.schema() != null ? serializeSchemaIpc(options.schema()) : null);
333+
}
334+
335+
/** Read an Arrow IPC file as a {@link DataFrame} without registering it. */
336+
public DataFrame readArrow(String path) {
337+
return readArrow(path, new ArrowReadOptions());
338+
}
339+
340+
/**
341+
* Read an Arrow IPC file as a {@link DataFrame} with the supplied {@link ArrowReadOptions}.
342+
*
343+
* @throws IllegalArgumentException if {@code path} or {@code options} is {@code null}.
344+
* @throws RuntimeException if the read fails.
345+
*/
346+
public DataFrame readArrow(String path, ArrowReadOptions options) {
347+
if (nativeHandle == 0) {
348+
throw new IllegalStateException("SessionContext is closed");
349+
}
350+
if (path == null) {
351+
throw new IllegalArgumentException("readArrow path must be non-null");
352+
}
353+
if (options == null) {
354+
throw new IllegalArgumentException("readArrow options must be non-null");
355+
}
356+
long dfHandle =
357+
readArrowWithOptions(
358+
nativeHandle,
359+
path,
360+
options.toBytes(),
361+
options.schema() != null ? serializeSchemaIpc(options.schema()) : null);
362+
return new DataFrame(dfHandle);
363+
}
364+
301365
/**
302366
* Register a Java-implemented scalar UDF. After registration, the function can be invoked by SQL
303367
* via the UDF's name or referenced in DataFusion plans deserialised with {@link #fromProto}.
@@ -373,6 +437,12 @@ private static native void registerCsvWithOptions(
373437
private static native long readCsvWithOptions(
374438
long handle, String path, byte[] optionsBytes, byte[] schemaIpcBytes);
375439

440+
private static native void registerArrowWithOptions(
441+
long handle, String name, String path, byte[] optionsBytes, byte[] schemaIpcBytes);
442+
443+
private static native long readArrowWithOptions(
444+
long handle, String path, byte[] optionsBytes, byte[] schemaIpcBytes);
445+
376446
private static native void registerJsonWithOptions(
377447
long handle, String name, String path, byte[] optionsBytes, byte[] schemaIpcBytes);
378448

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
package org.apache.datafusion;
21+
22+
import static org.junit.jupiter.api.Assertions.assertEquals;
23+
import static org.junit.jupiter.api.Assertions.assertSame;
24+
25+
import java.util.List;
26+
27+
import org.apache.arrow.vector.types.pojo.ArrowType;
28+
import org.apache.arrow.vector.types.pojo.Field;
29+
import org.apache.arrow.vector.types.pojo.FieldType;
30+
import org.apache.arrow.vector.types.pojo.Schema;
31+
import org.apache.datafusion.protobuf.ArrowReadOptionsProto;
32+
import org.junit.jupiter.api.Test;
33+
34+
import com.google.protobuf.InvalidProtocolBufferException;
35+
36+
class ArrowReadOptionsTest {
37+
38+
@Test
39+
void defaultsRoundTripThroughProto() throws InvalidProtocolBufferException {
40+
ArrowReadOptionsProto p = ArrowReadOptionsProto.parseFrom(new ArrowReadOptions().toBytes());
41+
assertEquals(".arrow", p.getFileExtension());
42+
}
43+
44+
@Test
45+
void fileExtensionRoundTripsThroughProto() throws InvalidProtocolBufferException {
46+
ArrowReadOptionsProto p =
47+
ArrowReadOptionsProto.parseFrom(new ArrowReadOptions().fileExtension(".ipc").toBytes());
48+
assertEquals(".ipc", p.getFileExtension());
49+
}
50+
51+
@Test
52+
void schemaIsHeldByReferenceAndNotInProto() {
53+
Schema schema =
54+
new Schema(List.of(new Field("x", FieldType.nullable(new ArrowType.Int(32, true)), null)));
55+
ArrowReadOptions opts = new ArrowReadOptions().schema(schema);
56+
assertSame(schema, opts.schema());
57+
}
58+
}
Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
package org.apache.datafusion;
21+
22+
import static org.junit.jupiter.api.Assertions.assertEquals;
23+
import static org.junit.jupiter.api.Assertions.assertThrows;
24+
import static org.junit.jupiter.api.Assertions.assertTrue;
25+
26+
import java.io.IOException;
27+
import java.nio.channels.FileChannel;
28+
import java.nio.file.Path;
29+
import java.nio.file.StandardOpenOption;
30+
import java.util.List;
31+
32+
import org.apache.arrow.memory.BufferAllocator;
33+
import org.apache.arrow.memory.RootAllocator;
34+
import org.apache.arrow.vector.BigIntVector;
35+
import org.apache.arrow.vector.IntVector;
36+
import org.apache.arrow.vector.VarCharVector;
37+
import org.apache.arrow.vector.VectorSchemaRoot;
38+
import org.apache.arrow.vector.ipc.ArrowFileWriter;
39+
import org.apache.arrow.vector.ipc.ArrowReader;
40+
import org.apache.arrow.vector.types.pojo.ArrowType;
41+
import org.apache.arrow.vector.types.pojo.Field;
42+
import org.apache.arrow.vector.types.pojo.FieldType;
43+
import org.apache.arrow.vector.types.pojo.Schema;
44+
import org.junit.jupiter.api.Test;
45+
import org.junit.jupiter.api.io.TempDir;
46+
47+
class SessionContextArrowTest {
48+
49+
/**
50+
* Write three rows of {@code (id INT, name UTF8)} as a single Arrow IPC file using arrow-vector's
51+
* built-in file writer. Returns the path the test can hand to {@code registerArrow} / {@code
52+
* readArrow}.
53+
*/
54+
private static Path writePeopleArrow(Path dir, String name) throws IOException {
55+
Schema schema =
56+
new Schema(
57+
List.of(
58+
new Field("id", FieldType.notNullable(new ArrowType.Int(32, true)), null),
59+
new Field("name", FieldType.notNullable(new ArrowType.Utf8()), null)));
60+
Path file = dir.resolve(name);
61+
try (BufferAllocator allocator = new RootAllocator();
62+
VectorSchemaRoot root = VectorSchemaRoot.create(schema, allocator)) {
63+
IntVector id = (IntVector) root.getVector("id");
64+
VarCharVector nameVec = (VarCharVector) root.getVector("name");
65+
id.allocateNew(3);
66+
nameVec.allocateNew(3);
67+
id.set(0, 1);
68+
id.set(1, 2);
69+
id.set(2, 3);
70+
nameVec.setSafe(0, "alice".getBytes());
71+
nameVec.setSafe(1, "bob".getBytes());
72+
nameVec.setSafe(2, "carol".getBytes());
73+
root.setRowCount(3);
74+
75+
try (FileChannel ch =
76+
FileChannel.open(file, StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE);
77+
ArrowFileWriter writer = new ArrowFileWriter(root, null, ch)) {
78+
writer.start();
79+
writer.writeBatch();
80+
writer.end();
81+
}
82+
}
83+
return file;
84+
}
85+
86+
@Test
87+
void registerArrowInfersSchemaAndCounts(@TempDir Path tempDir) throws Exception {
88+
Path file = writePeopleArrow(tempDir, "people.arrow");
89+
90+
try (BufferAllocator allocator = new RootAllocator();
91+
SessionContext ctx = new SessionContext()) {
92+
ctx.registerArrow("people", file.toAbsolutePath().toString());
93+
94+
try (DataFrame df = ctx.sql("SELECT COUNT(*) FROM people");
95+
ArrowReader reader = df.collect(allocator)) {
96+
assertTrue(reader.loadNextBatch());
97+
BigIntVector count = (BigIntVector) reader.getVectorSchemaRoot().getVector(0);
98+
assertEquals(3L, count.get(0));
99+
}
100+
101+
try (DataFrame df = ctx.sql("SELECT name FROM people WHERE id = 2");
102+
ArrowReader reader = df.collect(allocator)) {
103+
assertTrue(reader.loadNextBatch());
104+
VectorSchemaRoot root = reader.getVectorSchemaRoot();
105+
assertEquals(1, root.getRowCount());
106+
VarCharVector names = (VarCharVector) root.getVector(0);
107+
assertEquals("bob", new String(names.get(0)));
108+
}
109+
}
110+
}
111+
112+
@Test
113+
void readArrowYieldsTheStoredRows(@TempDir Path tempDir) throws Exception {
114+
Path file = writePeopleArrow(tempDir, "people.arrow");
115+
116+
try (BufferAllocator allocator = new RootAllocator();
117+
SessionContext ctx = new SessionContext();
118+
DataFrame df = ctx.readArrow(file.toAbsolutePath().toString());
119+
ArrowReader reader = df.collect(allocator)) {
120+
long total = 0;
121+
while (reader.loadNextBatch()) {
122+
total += reader.getVectorSchemaRoot().getRowCount();
123+
}
124+
assertEquals(3L, total);
125+
}
126+
}
127+
128+
@Test
129+
void registerArrowWithCustomExtension(@TempDir Path tempDir) throws Exception {
130+
Path file = writePeopleArrow(tempDir, "people.ipc");
131+
132+
try (BufferAllocator allocator = new RootAllocator();
133+
SessionContext ctx = new SessionContext()) {
134+
ctx.registerArrow(
135+
"t", file.toAbsolutePath().toString(), new ArrowReadOptions().fileExtension(".ipc"));
136+
137+
try (DataFrame df = ctx.sql("SELECT SUM(id) FROM t");
138+
ArrowReader reader = df.collect(allocator)) {
139+
assertTrue(reader.loadNextBatch());
140+
BigIntVector sum = (BigIntVector) reader.getVectorSchemaRoot().getVector(0);
141+
assertEquals(6L, sum.get(0));
142+
}
143+
}
144+
}
145+
146+
@Test
147+
void readArrowWithExplicitSchemaIsAccepted(@TempDir Path tempDir) throws Exception {
148+
// Explicit schema overrides on-read inference. We supply the same schema the
149+
// file actually has, so query results stay correct; the test pins that the
150+
// explicit-schema code path is plumbed through and accepted.
151+
Path file = writePeopleArrow(tempDir, "people.arrow");
152+
Schema schema =
153+
new Schema(
154+
List.of(
155+
new Field("id", FieldType.notNullable(new ArrowType.Int(32, true)), null),
156+
new Field("name", FieldType.notNullable(new ArrowType.Utf8()), null)));
157+
158+
try (BufferAllocator allocator = new RootAllocator();
159+
SessionContext ctx = new SessionContext();
160+
DataFrame df =
161+
ctx.readArrow(file.toAbsolutePath().toString(), new ArrowReadOptions().schema(schema));
162+
ArrowReader reader = df.collect(allocator)) {
163+
assertTrue(reader.loadNextBatch());
164+
VectorSchemaRoot root = reader.getVectorSchemaRoot();
165+
assertEquals(3, root.getRowCount());
166+
assertEquals("id", root.getSchema().getFields().get(0).getName());
167+
assertEquals("name", root.getSchema().getFields().get(1).getName());
168+
}
169+
}
170+
171+
@Test
172+
void registerArrowRejectsNullArguments() {
173+
try (SessionContext ctx = new SessionContext()) {
174+
ArrowReadOptions opts = new ArrowReadOptions();
175+
assertThrows(IllegalArgumentException.class, () -> ctx.registerArrow(null, "/p"));
176+
assertThrows(IllegalArgumentException.class, () -> ctx.registerArrow("t", null));
177+
assertThrows(IllegalArgumentException.class, () -> ctx.registerArrow(null, "/p", opts));
178+
assertThrows(IllegalArgumentException.class, () -> ctx.registerArrow("t", null, opts));
179+
assertThrows(IllegalArgumentException.class, () -> ctx.registerArrow("t", "/p", null));
180+
}
181+
}
182+
183+
@Test
184+
void readArrowRejectsNullArguments() {
185+
try (SessionContext ctx = new SessionContext()) {
186+
ArrowReadOptions opts = new ArrowReadOptions();
187+
assertThrows(IllegalArgumentException.class, () -> ctx.readArrow(null));
188+
assertThrows(IllegalArgumentException.class, () -> ctx.readArrow(null, opts));
189+
assertThrows(IllegalArgumentException.class, () -> ctx.readArrow("/p", null));
190+
}
191+
}
192+
}

0 commit comments

Comments
 (0)