feat(arrow): expose Arrow IPC reader via registerArrow and readArrow (#52)
diff --git a/core/src/main/java/org/apache/datafusion/ArrowReadOptions.java b/core/src/main/java/org/apache/datafusion/ArrowReadOptions.java
new file mode 100644
index 0000000..c48da4b
--- /dev/null
+++ b/core/src/main/java/org/apache/datafusion/ArrowReadOptions.java
@@ -0,0 +1,59 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.datafusion;
+
+import org.apache.arrow.vector.types.pojo.Schema;
+import org.apache.datafusion.protobuf.ArrowReadOptionsProto;
+
+/**
+ * Configuration knobs for Arrow IPC sources passed to {@link SessionContext#registerArrow(String,
+ * String, ArrowReadOptions)} and {@link SessionContext#readArrow(String, ArrowReadOptions)}.
+ *
+ * <p>Mirrors the subset of DataFusion's {@code ArrowReadOptions} that maps onto the Java surface
+ * today: {@code fileExtension} (default {@code ".arrow"}) and an explicit Arrow {@code schema} that
+ * bypasses on-read schema inference. {@code tablePartitionCols} is intentionally deferred --
+ * neither Parquet nor CSV expose Hive-style partitioning on the Java side yet.
+ *
+ * <p>Arrow IPC files carry their own body compression (LZ4_FRAME / ZSTD per-buffer) inside the file
+ * format itself, so unlike CSV / NDJSON there is no {@code FileCompressionType} setter.
+ */
+public final class ArrowReadOptions {
+
+  private String fileExtension = ".arrow";
+  private Schema schema;
+
+  public ArrowReadOptions fileExtension(String ext) {
+    this.fileExtension = ext;
+    return this;
+  }
+
+  public ArrowReadOptions schema(Schema schema) {
+    this.schema = schema;
+    return this;
+  }
+
+  byte[] toBytes() {
+    return ArrowReadOptionsProto.newBuilder().setFileExtension(fileExtension).build().toByteArray();
+  }
+
+  Schema schema() {
+    return schema;
+  }
+}
diff --git a/core/src/main/java/org/apache/datafusion/SessionContext.java b/core/src/main/java/org/apache/datafusion/SessionContext.java
index c4d8052..328eb6d 100644
--- a/core/src/main/java/org/apache/datafusion/SessionContext.java
+++ b/core/src/main/java/org/apache/datafusion/SessionContext.java
@@ -298,6 +298,70 @@
     return new DataFrame(dfHandle);
   }
 
+  /** Register an Arrow IPC file (or directory of Arrow IPC files) as a table. */
+  public void registerArrow(String name, String path) {
+    registerArrow(name, path, new ArrowReadOptions());
+  }
+
+  /**
+   * Register an Arrow IPC file (or directory of Arrow IPC files) as a table with the supplied
+   * {@link ArrowReadOptions}.
+   *
+   * @throws IllegalArgumentException if any of {@code name}, {@code path}, or {@code options} is
+   *     {@code null}.
+   * @throws RuntimeException if registration fails (path not found, schema mismatch, etc.).
+   */
+  public void registerArrow(String name, String path, ArrowReadOptions options) {
+    if (nativeHandle == 0) {
+      throw new IllegalStateException("SessionContext is closed");
+    }
+    if (name == null) {
+      throw new IllegalArgumentException("registerArrow name must be non-null");
+    }
+    if (path == null) {
+      throw new IllegalArgumentException("registerArrow path must be non-null");
+    }
+    if (options == null) {
+      throw new IllegalArgumentException("registerArrow options must be non-null");
+    }
+    registerArrowWithOptions(
+        nativeHandle,
+        name,
+        path,
+        options.toBytes(),
+        options.schema() != null ? serializeSchemaIpc(options.schema()) : null);
+  }
+
+  /** Read an Arrow IPC file as a {@link DataFrame} without registering it. */
+  public DataFrame readArrow(String path) {
+    return readArrow(path, new ArrowReadOptions());
+  }
+
+  /**
+   * Read an Arrow IPC file as a {@link DataFrame} with the supplied {@link ArrowReadOptions}.
+   *
+   * @throws IllegalArgumentException if {@code path} or {@code options} is {@code null}.
+   * @throws RuntimeException if the read fails.
+   */
+  public DataFrame readArrow(String path, ArrowReadOptions options) {
+    if (nativeHandle == 0) {
+      throw new IllegalStateException("SessionContext is closed");
+    }
+    if (path == null) {
+      throw new IllegalArgumentException("readArrow path must be non-null");
+    }
+    if (options == null) {
+      throw new IllegalArgumentException("readArrow options must be non-null");
+    }
+    long dfHandle =
+        readArrowWithOptions(
+            nativeHandle,
+            path,
+            options.toBytes(),
+            options.schema() != null ? serializeSchemaIpc(options.schema()) : null);
+    return new DataFrame(dfHandle);
+  }
+
   /**
    * Register a Java-implemented scalar UDF. After registration, the function can be invoked by SQL
    * via the UDF's name or referenced in DataFusion plans deserialised with {@link #fromProto}.
@@ -373,6 +437,12 @@
   private static native long readCsvWithOptions(
       long handle, String path, byte[] optionsBytes, byte[] schemaIpcBytes);
 
+  private static native void registerArrowWithOptions(
+      long handle, String name, String path, byte[] optionsBytes, byte[] schemaIpcBytes);
+
+  private static native long readArrowWithOptions(
+      long handle, String path, byte[] optionsBytes, byte[] schemaIpcBytes);
+
   private static native void registerJsonWithOptions(
       long handle, String name, String path, byte[] optionsBytes, byte[] schemaIpcBytes);
 
diff --git a/core/src/test/java/org/apache/datafusion/ArrowReadOptionsTest.java b/core/src/test/java/org/apache/datafusion/ArrowReadOptionsTest.java
new file mode 100644
index 0000000..db84334
--- /dev/null
+++ b/core/src/test/java/org/apache/datafusion/ArrowReadOptionsTest.java
@@ -0,0 +1,58 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.datafusion;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertSame;
+
+import java.util.List;
+
+import org.apache.arrow.vector.types.pojo.ArrowType;
+import org.apache.arrow.vector.types.pojo.Field;
+import org.apache.arrow.vector.types.pojo.FieldType;
+import org.apache.arrow.vector.types.pojo.Schema;
+import org.apache.datafusion.protobuf.ArrowReadOptionsProto;
+import org.junit.jupiter.api.Test;
+
+import com.google.protobuf.InvalidProtocolBufferException;
+
+class ArrowReadOptionsTest {
+
+  @Test
+  void defaultsRoundTripThroughProto() throws InvalidProtocolBufferException {
+    ArrowReadOptionsProto p = ArrowReadOptionsProto.parseFrom(new ArrowReadOptions().toBytes());
+    assertEquals(".arrow", p.getFileExtension());
+  }
+
+  @Test
+  void fileExtensionRoundTripsThroughProto() throws InvalidProtocolBufferException {
+    ArrowReadOptionsProto p =
+        ArrowReadOptionsProto.parseFrom(new ArrowReadOptions().fileExtension(".ipc").toBytes());
+    assertEquals(".ipc", p.getFileExtension());
+  }
+
+  @Test
+  void schemaIsHeldByReferenceAndNotInProto() {
+    Schema schema =
+        new Schema(List.of(new Field("x", FieldType.nullable(new ArrowType.Int(32, true)), null)));
+    ArrowReadOptions opts = new ArrowReadOptions().schema(schema);
+    assertSame(schema, opts.schema());
+  }
+}
diff --git a/core/src/test/java/org/apache/datafusion/SessionContextArrowTest.java b/core/src/test/java/org/apache/datafusion/SessionContextArrowTest.java
new file mode 100644
index 0000000..b0efc07
--- /dev/null
+++ b/core/src/test/java/org/apache/datafusion/SessionContextArrowTest.java
@@ -0,0 +1,192 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.datafusion;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.IOException;
+import java.nio.channels.FileChannel;
+import java.nio.file.Path;
+import java.nio.file.StandardOpenOption;
+import java.util.List;
+
+import org.apache.arrow.memory.BufferAllocator;
+import org.apache.arrow.memory.RootAllocator;
+import org.apache.arrow.vector.BigIntVector;
+import org.apache.arrow.vector.IntVector;
+import org.apache.arrow.vector.VarCharVector;
+import org.apache.arrow.vector.VectorSchemaRoot;
+import org.apache.arrow.vector.ipc.ArrowFileWriter;
+import org.apache.arrow.vector.ipc.ArrowReader;
+import org.apache.arrow.vector.types.pojo.ArrowType;
+import org.apache.arrow.vector.types.pojo.Field;
+import org.apache.arrow.vector.types.pojo.FieldType;
+import org.apache.arrow.vector.types.pojo.Schema;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+class SessionContextArrowTest {
+
+  /**
+   * Write three rows of {@code (id INT, name UTF8)} as a single Arrow IPC file using arrow-vector's
+   * built-in file writer. Returns the path the test can hand to {@code registerArrow} / {@code
+   * readArrow}.
+   */
+  private static Path writePeopleArrow(Path dir, String name) throws IOException {
+    Schema schema =
+        new Schema(
+            List.of(
+                new Field("id", FieldType.notNullable(new ArrowType.Int(32, true)), null),
+                new Field("name", FieldType.notNullable(new ArrowType.Utf8()), null)));
+    Path file = dir.resolve(name);
+    try (BufferAllocator allocator = new RootAllocator();
+        VectorSchemaRoot root = VectorSchemaRoot.create(schema, allocator)) {
+      IntVector id = (IntVector) root.getVector("id");
+      VarCharVector nameVec = (VarCharVector) root.getVector("name");
+      id.allocateNew(3);
+      nameVec.allocateNew(3);
+      id.set(0, 1);
+      id.set(1, 2);
+      id.set(2, 3);
+      nameVec.setSafe(0, "alice".getBytes());
+      nameVec.setSafe(1, "bob".getBytes());
+      nameVec.setSafe(2, "carol".getBytes());
+      root.setRowCount(3);
+
+      try (FileChannel ch =
+              FileChannel.open(file, StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE);
+          ArrowFileWriter writer = new ArrowFileWriter(root, null, ch)) {
+        writer.start();
+        writer.writeBatch();
+        writer.end();
+      }
+    }
+    return file;
+  }
+
+  @Test
+  void registerArrowInfersSchemaAndCounts(@TempDir Path tempDir) throws Exception {
+    Path file = writePeopleArrow(tempDir, "people.arrow");
+
+    try (BufferAllocator allocator = new RootAllocator();
+        SessionContext ctx = new SessionContext()) {
+      ctx.registerArrow("people", file.toAbsolutePath().toString());
+
+      try (DataFrame df = ctx.sql("SELECT COUNT(*) FROM people");
+          ArrowReader reader = df.collect(allocator)) {
+        assertTrue(reader.loadNextBatch());
+        BigIntVector count = (BigIntVector) reader.getVectorSchemaRoot().getVector(0);
+        assertEquals(3L, count.get(0));
+      }
+
+      try (DataFrame df = ctx.sql("SELECT name FROM people WHERE id = 2");
+          ArrowReader reader = df.collect(allocator)) {
+        assertTrue(reader.loadNextBatch());
+        VectorSchemaRoot root = reader.getVectorSchemaRoot();
+        assertEquals(1, root.getRowCount());
+        VarCharVector names = (VarCharVector) root.getVector(0);
+        assertEquals("bob", new String(names.get(0)));
+      }
+    }
+  }
+
+  @Test
+  void readArrowYieldsTheStoredRows(@TempDir Path tempDir) throws Exception {
+    Path file = writePeopleArrow(tempDir, "people.arrow");
+
+    try (BufferAllocator allocator = new RootAllocator();
+        SessionContext ctx = new SessionContext();
+        DataFrame df = ctx.readArrow(file.toAbsolutePath().toString());
+        ArrowReader reader = df.collect(allocator)) {
+      long total = 0;
+      while (reader.loadNextBatch()) {
+        total += reader.getVectorSchemaRoot().getRowCount();
+      }
+      assertEquals(3L, total);
+    }
+  }
+
+  @Test
+  void registerArrowWithCustomExtension(@TempDir Path tempDir) throws Exception {
+    Path file = writePeopleArrow(tempDir, "people.ipc");
+
+    try (BufferAllocator allocator = new RootAllocator();
+        SessionContext ctx = new SessionContext()) {
+      ctx.registerArrow(
+          "t", file.toAbsolutePath().toString(), new ArrowReadOptions().fileExtension(".ipc"));
+
+      try (DataFrame df = ctx.sql("SELECT SUM(id) FROM t");
+          ArrowReader reader = df.collect(allocator)) {
+        assertTrue(reader.loadNextBatch());
+        BigIntVector sum = (BigIntVector) reader.getVectorSchemaRoot().getVector(0);
+        assertEquals(6L, sum.get(0));
+      }
+    }
+  }
+
+  @Test
+  void readArrowWithExplicitSchemaIsAccepted(@TempDir Path tempDir) throws Exception {
+    // Explicit schema overrides on-read inference. We supply the same schema the
+    // file actually has, so query results stay correct; the test pins that the
+    // explicit-schema code path is plumbed through and accepted.
+    Path file = writePeopleArrow(tempDir, "people.arrow");
+    Schema schema =
+        new Schema(
+            List.of(
+                new Field("id", FieldType.notNullable(new ArrowType.Int(32, true)), null),
+                new Field("name", FieldType.notNullable(new ArrowType.Utf8()), null)));
+
+    try (BufferAllocator allocator = new RootAllocator();
+        SessionContext ctx = new SessionContext();
+        DataFrame df =
+            ctx.readArrow(file.toAbsolutePath().toString(), new ArrowReadOptions().schema(schema));
+        ArrowReader reader = df.collect(allocator)) {
+      assertTrue(reader.loadNextBatch());
+      VectorSchemaRoot root = reader.getVectorSchemaRoot();
+      assertEquals(3, root.getRowCount());
+      assertEquals("id", root.getSchema().getFields().get(0).getName());
+      assertEquals("name", root.getSchema().getFields().get(1).getName());
+    }
+  }
+
+  @Test
+  void registerArrowRejectsNullArguments() {
+    try (SessionContext ctx = new SessionContext()) {
+      ArrowReadOptions opts = new ArrowReadOptions();
+      assertThrows(IllegalArgumentException.class, () -> ctx.registerArrow(null, "/p"));
+      assertThrows(IllegalArgumentException.class, () -> ctx.registerArrow("t", null));
+      assertThrows(IllegalArgumentException.class, () -> ctx.registerArrow(null, "/p", opts));
+      assertThrows(IllegalArgumentException.class, () -> ctx.registerArrow("t", null, opts));
+      assertThrows(IllegalArgumentException.class, () -> ctx.registerArrow("t", "/p", null));
+    }
+  }
+
+  @Test
+  void readArrowRejectsNullArguments() {
+    try (SessionContext ctx = new SessionContext()) {
+      ArrowReadOptions opts = new ArrowReadOptions();
+      assertThrows(IllegalArgumentException.class, () -> ctx.readArrow(null));
+      assertThrows(IllegalArgumentException.class, () -> ctx.readArrow(null, opts));
+      assertThrows(IllegalArgumentException.class, () -> ctx.readArrow("/p", null));
+    }
+  }
+}
diff --git a/native/build.rs b/native/build.rs
index 7c164d5..92146b3 100644
--- a/native/build.rs
+++ b/native/build.rs
@@ -19,6 +19,7 @@
     const PROTOS: &[&str] = &[
         "../proto/session_options.proto",
         "../proto/file_compression_type.proto",
+        "../proto/arrow_read_options.proto",
         "../proto/csv_read_options.proto",
         "../proto/json_read_options.proto",
         "../proto/parquet_read_options.proto",
diff --git a/native/src/arrow.rs b/native/src/arrow.rs
new file mode 100644
index 0000000..2bbe7b0
--- /dev/null
+++ b/native/src/arrow.rs
@@ -0,0 +1,106 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use datafusion::error::DataFusionError;
+use datafusion::execution::options::ArrowReadOptions;
+use datafusion::prelude::SessionContext;
+use jni::objects::{JByteArray, JClass, JString};
+use jni::sys::jlong;
+use jni::JNIEnv;
+use prost::Message;
+
+use crate::errors::{try_unwrap_or_throw, JniResult};
+use crate::proto_gen::ArrowReadOptionsProto;
+use crate::runtime;
+use crate::schema::decode_optional_schema;
+
+fn with_arrow_options<R>(
+    env: &mut JNIEnv,
+    options_bytes: JByteArray,
+    schema_ipc_bytes: JByteArray,
+    f: impl FnOnce(ArrowReadOptions) -> JniResult<R>,
+) -> JniResult<R> {
+    let bytes: Vec<u8> = env.convert_byte_array(&options_bytes)?;
+    let p = ArrowReadOptionsProto::decode(bytes.as_slice())?;
+
+    let schema = decode_optional_schema(env, schema_ipc_bytes)?;
+
+    // ArrowReadOptions exposes `file_extension` as a public field (not a builder
+    // setter); `schema` is the only field with a fluent setter. Build via
+    // struct-update syntax to avoid clippy::field_reassign_with_default.
+    let file_ext = p.file_extension;
+    let mut opts = ArrowReadOptions {
+        file_extension: &file_ext,
+        ..ArrowReadOptions::default()
+    };
+    if let Some(ref s) = schema {
+        opts = opts.schema(s);
+    }
+
+    f(opts)
+}
+
+#[no_mangle]
+pub extern "system" fn Java_org_apache_datafusion_SessionContext_registerArrowWithOptions<
+    'local,
+>(
+    mut env: JNIEnv<'local>,
+    _class: JClass<'local>,
+    handle: jlong,
+    name: JString<'local>,
+    path: JString<'local>,
+    options_bytes: JByteArray<'local>,
+    schema_ipc_bytes: JByteArray<'local>,
+) {
+    try_unwrap_or_throw(&mut env, (), |env| -> JniResult<()> {
+        if handle == 0 {
+            return Err("SessionContext handle is null".into());
+        }
+        let ctx = unsafe { &*(handle as *const SessionContext) };
+        let name: String = env.get_string(&name)?.into();
+        let path: String = env.get_string(&path)?.into();
+        with_arrow_options(env, options_bytes, schema_ipc_bytes, |opts| {
+            runtime().block_on(async {
+                ctx.register_arrow(&name, &path, opts).await?;
+                Ok::<(), DataFusionError>(())
+            })?;
+            Ok(())
+        })
+    })
+}
+
+#[no_mangle]
+pub extern "system" fn Java_org_apache_datafusion_SessionContext_readArrowWithOptions<'local>(
+    mut env: JNIEnv<'local>,
+    _class: JClass<'local>,
+    handle: jlong,
+    path: JString<'local>,
+    options_bytes: JByteArray<'local>,
+    schema_ipc_bytes: JByteArray<'local>,
+) -> jlong {
+    try_unwrap_or_throw(&mut env, 0, |env| -> JniResult<jlong> {
+        if handle == 0 {
+            return Err("SessionContext handle is null".into());
+        }
+        let ctx = unsafe { &*(handle as *const SessionContext) };
+        let path: String = env.get_string(&path)?.into();
+        with_arrow_options(env, options_bytes, schema_ipc_bytes, |opts| {
+            let df = runtime().block_on(ctx.read_arrow(path, opts))?;
+            Ok(Box::into_raw(Box::new(df)) as jlong)
+        })
+    })
+}
diff --git a/native/src/lib.rs b/native/src/lib.rs
index bb2d8b4..f6f16d3 100644
--- a/native/src/lib.rs
+++ b/native/src/lib.rs
@@ -15,6 +15,7 @@
 // specific language governing permissions and limitations
 // under the License.
 
+mod arrow;
 mod csv;
 mod errors;
 mod json;
diff --git a/proto/arrow_read_options.proto b/proto/arrow_read_options.proto
new file mode 100644
index 0000000..4be8cc3
--- /dev/null
+++ b/proto/arrow_read_options.proto
@@ -0,0 +1,35 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+syntax = "proto3";
+
+package datafusion_java;
+
+option java_package = "org.apache.datafusion.protobuf";
+option java_multiple_files = true;
+
+// Options used to read Arrow IPC files. `file_extension` has a non-null Java
+// default and is always sent. The explicit Arrow schema, if present, is
+// transferred separately as Arrow IPC bytes through the JNI layer (mirroring
+// the parquet, csv, and ndjson paths) and is not encoded in this message.
+//
+// Arrow IPC files carry their own body compression (LZ4_FRAME / ZSTD,
+// per-buffer rather than per-file), so unlike CSV/JSON there is no
+// `FileCompressionType` field here.
+message ArrowReadOptionsProto {
+  string file_extension = 1;
+}