feat(parquet): expose ParquetReadOptions via registerParquet and readParquet (#18)
diff --git a/native/src/lib.rs b/native/src/lib.rs
index 947d47f..cc58b7d 100644
--- a/native/src/lib.rs
+++ b/native/src/lib.rs
@@ -19,14 +19,15 @@
use std::sync::{Arc, OnceLock};
-use datafusion::arrow::datatypes::SchemaRef;
+use datafusion::arrow::datatypes::{Schema, SchemaRef};
use datafusion::arrow::ffi_stream::FFI_ArrowArrayStream;
+use datafusion::arrow::ipc::reader::StreamReader;
use datafusion::arrow::record_batch::RecordBatchIterator;
use datafusion::dataframe::DataFrame;
use datafusion::error::DataFusionError;
use datafusion::prelude::{ParquetReadOptions, SessionContext};
-use jni::objects::{JClass, JString};
-use jni::sys::jlong;
+use jni::objects::{JByteArray, JClass, JString};
+use jni::sys::{jboolean, jlong};
use jni::JNIEnv;
use tokio::runtime::Runtime;
@@ -129,13 +130,61 @@
})
}
+#[allow(clippy::too_many_arguments)]
+fn with_parquet_options<R>(
+ env: &mut JNIEnv,
+ file_extension: JString,
+ parquet_pruning_set: jboolean,
+ parquet_pruning_value: jboolean,
+ skip_metadata_set: jboolean,
+ skip_metadata_value: jboolean,
+ metadata_size_hint: jlong,
+ schema_ipc_bytes: JByteArray,
+ f: impl FnOnce(ParquetReadOptions) -> JniResult<R>,
+) -> JniResult<R> {
+ let file_ext: String = env.get_string(&file_extension)?.into();
+
+ let schema: Option<Schema> = if !schema_ipc_bytes.is_null() {
+ let bytes: Vec<u8> = env.convert_byte_array(&schema_ipc_bytes)?;
+ let reader = StreamReader::try_new(std::io::Cursor::new(bytes), None)?;
+ Some((*reader.schema()).clone())
+ } else {
+ None
+ };
+
+ let mut opts = ParquetReadOptions::default().file_extension(&file_ext);
+ if parquet_pruning_set != 0 {
+ opts = opts.parquet_pruning(parquet_pruning_value != 0);
+ }
+ if skip_metadata_set != 0 {
+ opts = opts.skip_metadata(skip_metadata_value != 0);
+ }
+ if metadata_size_hint >= 0 {
+ opts = opts.metadata_size_hint(Some(metadata_size_hint as usize));
+ }
+ if let Some(ref s) = schema {
+ opts = opts.schema(s);
+ }
+
+ f(opts)
+}
+
#[no_mangle]
-pub extern "system" fn Java_org_apache_datafusion_SessionContext_registerParquet<'local>(
+pub extern "system" fn Java_org_apache_datafusion_SessionContext_registerParquetWithOptions<
+ 'local,
+>(
mut env: JNIEnv<'local>,
_class: JClass<'local>,
handle: jlong,
name: JString<'local>,
path: JString<'local>,
+ file_extension: JString<'local>,
+ parquet_pruning_set: jboolean,
+ parquet_pruning_value: jboolean,
+ skip_metadata_set: jboolean,
+ skip_metadata_value: jboolean,
+ metadata_size_hint: jlong,
+ schema_ipc_bytes: JByteArray<'local>,
) {
try_unwrap_or_throw(&mut env, (), |env| -> JniResult<()> {
if handle == 0 {
@@ -144,11 +193,59 @@
let ctx = unsafe { &*(handle as *const SessionContext) };
let name: String = env.get_string(&name)?.into();
let path: String = env.get_string(&path)?.into();
- runtime().block_on(async {
- ctx.register_parquet(&name, &path, ParquetReadOptions::default())
- .await?;
- Ok::<(), DataFusionError>(())
- })?;
- Ok(())
+ with_parquet_options(
+ env,
+ file_extension,
+ parquet_pruning_set,
+ parquet_pruning_value,
+ skip_metadata_set,
+ skip_metadata_value,
+ metadata_size_hint,
+ schema_ipc_bytes,
+ |opts| {
+ runtime().block_on(async {
+ ctx.register_parquet(&name, &path, opts).await?;
+ Ok::<(), DataFusionError>(())
+ })?;
+ Ok(())
+ },
+ )
+ })
+}
+
+#[no_mangle]
+pub extern "system" fn Java_org_apache_datafusion_SessionContext_readParquetWithOptions<'local>(
+ mut env: JNIEnv<'local>,
+ _class: JClass<'local>,
+ handle: jlong,
+ path: JString<'local>,
+ file_extension: JString<'local>,
+ parquet_pruning_set: jboolean,
+ parquet_pruning_value: jboolean,
+ skip_metadata_set: jboolean,
+ skip_metadata_value: jboolean,
+ metadata_size_hint: jlong,
+ 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_parquet_options(
+ env,
+ file_extension,
+ parquet_pruning_set,
+ parquet_pruning_value,
+ skip_metadata_set,
+ skip_metadata_value,
+ metadata_size_hint,
+ schema_ipc_bytes,
+ |opts| {
+ let df = runtime().block_on(ctx.read_parquet(path, opts))?;
+ Ok(Box::into_raw(Box::new(df)) as jlong)
+ },
+ )
})
}
diff --git a/src/main/java/org/apache/datafusion/ParquetReadOptions.java b/src/main/java/org/apache/datafusion/ParquetReadOptions.java
new file mode 100644
index 0000000..b29d762
--- /dev/null
+++ b/src/main/java/org/apache/datafusion/ParquetReadOptions.java
@@ -0,0 +1,84 @@
+/*
+ * 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;
+
+/**
+ * Configuration knobs for parquet sources passed to {@link SessionContext#registerParquet(String,
+ * String, ParquetReadOptions)} and {@link SessionContext#readParquet(String, ParquetReadOptions)}.
+ *
+ * <p>Mirrors a subset of DataFusion's {@code ParquetReadOptions}. All setters return {@code this}
+ * for fluent chaining. Defaults: {@code fileExtension = ".parquet"}; all other fields {@code null}
+ * (meaning the SessionConfig default is used, or the schema is inferred from the file).
+ */
+public final class ParquetReadOptions {
+
+ private String fileExtension = ".parquet";
+ private Boolean parquetPruning;
+ private Boolean skipMetadata;
+ private Long metadataSizeHint;
+ private Schema schema;
+
+ public ParquetReadOptions fileExtension(String ext) {
+ this.fileExtension = ext;
+ return this;
+ }
+
+ public ParquetReadOptions parquetPruning(boolean v) {
+ this.parquetPruning = v;
+ return this;
+ }
+
+ public ParquetReadOptions skipMetadata(boolean v) {
+ this.skipMetadata = v;
+ return this;
+ }
+
+ public ParquetReadOptions metadataSizeHint(long bytes) {
+ this.metadataSizeHint = bytes;
+ return this;
+ }
+
+ public ParquetReadOptions schema(Schema schema) {
+ this.schema = schema;
+ return this;
+ }
+
+ String fileExtension() {
+ return fileExtension;
+ }
+
+ Boolean parquetPruning() {
+ return parquetPruning;
+ }
+
+ Boolean skipMetadata() {
+ return skipMetadata;
+ }
+
+ Long metadataSizeHint() {
+ return metadataSizeHint;
+ }
+
+ Schema schema() {
+ return schema;
+ }
+}
diff --git a/src/main/java/org/apache/datafusion/SessionContext.java b/src/main/java/org/apache/datafusion/SessionContext.java
index ac1a45e..fb79d43 100644
--- a/src/main/java/org/apache/datafusion/SessionContext.java
+++ b/src/main/java/org/apache/datafusion/SessionContext.java
@@ -19,6 +19,16 @@
package org.apache.datafusion;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.nio.channels.Channels;
+
+import org.apache.arrow.memory.BufferAllocator;
+import org.apache.arrow.memory.RootAllocator;
+import org.apache.arrow.vector.VectorSchemaRoot;
+import org.apache.arrow.vector.ipc.ArrowStreamWriter;
+import org.apache.arrow.vector.types.pojo.Schema;
+
/**
* A DataFusion session context.
*
@@ -54,10 +64,70 @@
}
public void registerParquet(String name, String path) {
+ registerParquet(name, path, new ParquetReadOptions());
+ }
+
+ /**
+ * Register a parquet file as a table with the supplied {@link ParquetReadOptions}.
+ *
+ * @throws RuntimeException if registration fails (path not found, schema mismatch, etc.).
+ */
+ public void registerParquet(String name, String path, ParquetReadOptions options) {
if (nativeHandle == 0) {
throw new IllegalStateException("SessionContext is closed");
}
- registerParquet(nativeHandle, name, path);
+ registerParquetWithOptions(
+ nativeHandle,
+ name,
+ path,
+ options.fileExtension(),
+ options.parquetPruning() != null,
+ options.parquetPruning() != null && options.parquetPruning(),
+ options.skipMetadata() != null,
+ options.skipMetadata() != null && options.skipMetadata(),
+ options.metadataSizeHint() != null ? options.metadataSizeHint() : -1L,
+ options.schema() != null ? serializeSchemaIpc(options.schema()) : null);
+ }
+
+ /** Read a parquet file as a {@link DataFrame} without registering it. */
+ public DataFrame readParquet(String path) {
+ return readParquet(path, new ParquetReadOptions());
+ }
+
+ /**
+ * Read a parquet file as a {@link DataFrame} with the supplied {@link ParquetReadOptions}.
+ *
+ * @throws RuntimeException if the read fails.
+ */
+ public DataFrame readParquet(String path, ParquetReadOptions options) {
+ if (nativeHandle == 0) {
+ throw new IllegalStateException("SessionContext is closed");
+ }
+ long dfHandle =
+ readParquetWithOptions(
+ nativeHandle,
+ path,
+ options.fileExtension(),
+ options.parquetPruning() != null,
+ options.parquetPruning() != null && options.parquetPruning(),
+ options.skipMetadata() != null,
+ options.skipMetadata() != null && options.skipMetadata(),
+ options.metadataSizeHint() != null ? options.metadataSizeHint() : -1L,
+ options.schema() != null ? serializeSchemaIpc(options.schema()) : null);
+ return new DataFrame(dfHandle);
+ }
+
+ private static byte[] serializeSchemaIpc(Schema schema) {
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ try (BufferAllocator allocator = new RootAllocator();
+ VectorSchemaRoot root = VectorSchemaRoot.create(schema, allocator);
+ ArrowStreamWriter writer = new ArrowStreamWriter(root, null, Channels.newChannel(baos))) {
+ writer.start();
+ writer.end();
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to serialize Arrow schema for JNI", e);
+ }
+ return baos.toByteArray();
}
@Override
@@ -72,7 +142,28 @@
private static native long createDataFrame(long handle, String sql);
- private static native void registerParquet(long handle, String name, String path);
+ private static native void registerParquetWithOptions(
+ long handle,
+ String name,
+ String path,
+ String fileExtension,
+ boolean parquetPruningSet,
+ boolean parquetPruningValue,
+ boolean skipMetadataSet,
+ boolean skipMetadataValue,
+ long metadataSizeHint,
+ byte[] schemaIpcBytes);
+
+ private static native long readParquetWithOptions(
+ long handle,
+ String path,
+ String fileExtension,
+ boolean parquetPruningSet,
+ boolean parquetPruningValue,
+ boolean skipMetadataSet,
+ boolean skipMetadataValue,
+ long metadataSizeHint,
+ byte[] schemaIpcBytes);
private static native void closeSessionContext(long handle);
}
diff --git a/src/test/java/org/apache/datafusion/ParquetReadOptionsTest.java b/src/test/java/org/apache/datafusion/ParquetReadOptionsTest.java
new file mode 100644
index 0000000..3eca311
--- /dev/null
+++ b/src/test/java/org/apache/datafusion/ParquetReadOptionsTest.java
@@ -0,0 +1,73 @@
+/*
+ * 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.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+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.junit.jupiter.api.Test;
+
+class ParquetReadOptionsTest {
+
+ @Test
+ void defaultsMatchDataFusion() {
+ ParquetReadOptions opts = new ParquetReadOptions();
+ assertEquals(".parquet", opts.fileExtension());
+ assertNull(opts.parquetPruning());
+ assertNull(opts.skipMetadata());
+ assertNull(opts.metadataSizeHint());
+ assertNull(opts.schema());
+ }
+
+ @Test
+ void fluentSettersChainAndMutate() {
+ Schema schema =
+ new Schema(List.of(new Field("x", FieldType.nullable(new ArrowType.Int(32, true)), null)));
+
+ ParquetReadOptions opts =
+ new ParquetReadOptions()
+ .fileExtension(".parq")
+ .parquetPruning(true)
+ .skipMetadata(false)
+ .metadataSizeHint(1_048_576L)
+ .schema(schema);
+
+ assertEquals(".parq", opts.fileExtension());
+ assertEquals(Boolean.TRUE, opts.parquetPruning());
+ assertEquals(Boolean.FALSE, opts.skipMetadata());
+ assertEquals(Long.valueOf(1_048_576L), opts.metadataSizeHint());
+ assertTrue(opts.schema() == schema);
+ }
+
+ @Test
+ void schemaSetterRetainsReferenceIdentity() {
+ Schema schema = new Schema(List.of());
+ ParquetReadOptions opts = new ParquetReadOptions().schema(schema);
+ assertSame(schema, opts.schema());
+ }
+}
diff --git a/src/test/java/org/apache/datafusion/SessionContextParquetOptionsTest.java b/src/test/java/org/apache/datafusion/SessionContextParquetOptionsTest.java
new file mode 100644
index 0000000..9b79327
--- /dev/null
+++ b/src/test/java/org/apache/datafusion/SessionContextParquetOptionsTest.java
@@ -0,0 +1,134 @@
+/*
+ * 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.assertTrue;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardCopyOption;
+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.VectorSchemaRoot;
+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.Assumptions;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+class SessionContextParquetOptionsTest {
+
+ @Test
+ void readParquetWithDefaultOptionsCountsAllRows() throws Exception {
+ Path lineitem = Path.of("tpch-data/sf1/lineitem.parquet");
+ Assumptions.assumeTrue(
+ Files.exists(lineitem), "TPC-H SF1 data not found; run `make tpch-data` first");
+
+ try (BufferAllocator allocator = new RootAllocator();
+ SessionContext ctx = new SessionContext();
+ DataFrame df = ctx.readParquet(lineitem.toAbsolutePath().toString());
+ ArrowReader reader = df.collect(allocator)) {
+ long total = 0;
+ while (reader.loadNextBatch()) {
+ VectorSchemaRoot root = reader.getVectorSchemaRoot();
+ total += root.getRowCount();
+ }
+ assertEquals(6_001_215L, total);
+ assertTrue(total > 0);
+ }
+ }
+
+ @Test
+ void registerParquetWithOptionsRespectsCustomExtension(@TempDir Path tempDir) throws Exception {
+ Path lineitem = Path.of("tpch-data/sf1/lineitem.parquet");
+ Assumptions.assumeTrue(
+ Files.exists(lineitem), "TPC-H SF1 data not found; run `make tpch-data` first");
+ Path renamed = tempDir.resolve("lineitem.parq");
+ Files.copy(lineitem, renamed, StandardCopyOption.REPLACE_EXISTING);
+
+ try (BufferAllocator allocator = new RootAllocator();
+ SessionContext ctx = new SessionContext()) {
+ ctx.registerParquet(
+ "t",
+ renamed.toAbsolutePath().toString(),
+ new ParquetReadOptions().fileExtension(".parq"));
+ try (DataFrame df = ctx.sql("SELECT COUNT(*) FROM t");
+ ArrowReader reader = df.collect(allocator)) {
+ assertTrue(reader.loadNextBatch());
+ VectorSchemaRoot root = reader.getVectorSchemaRoot();
+ assertEquals(1, root.getRowCount());
+ BigIntVector count = (BigIntVector) root.getVector(0);
+ assertEquals(6_001_215L, count.get(0));
+ }
+ }
+ }
+
+ @Test
+ void readParquetWithExplicitSchemaUsesProvidedSchema() throws Exception {
+ Path lineitem = Path.of("tpch-data/sf1/lineitem.parquet");
+ Assumptions.assumeTrue(
+ Files.exists(lineitem), "TPC-H SF1 data not found; run `make tpch-data` first");
+
+ Schema custom =
+ new Schema(
+ List.of(
+ new Field("l_orderkey", FieldType.notNullable(new ArrowType.Int(64, true)), null)));
+
+ try (BufferAllocator allocator = new RootAllocator();
+ SessionContext ctx = new SessionContext();
+ DataFrame df =
+ ctx.readParquet(
+ lineitem.toAbsolutePath().toString(), new ParquetReadOptions().schema(custom));
+ ArrowReader reader = df.collect(allocator)) {
+ assertTrue(reader.loadNextBatch());
+ Schema observed = reader.getVectorSchemaRoot().getSchema();
+ assertEquals(1, observed.getFields().size());
+ assertEquals("l_orderkey", observed.getFields().get(0).getName());
+ }
+ }
+
+ @Test
+ void registerParquetWithMetadataSizeHintIsAccepted() throws Exception {
+ Path lineitem = Path.of("tpch-data/sf1/lineitem.parquet");
+ Assumptions.assumeTrue(
+ Files.exists(lineitem), "TPC-H SF1 data not found; run `make tpch-data` first");
+
+ try (BufferAllocator allocator = new RootAllocator();
+ SessionContext ctx = new SessionContext()) {
+ ctx.registerParquet(
+ "t",
+ lineitem.toAbsolutePath().toString(),
+ new ParquetReadOptions().metadataSizeHint(1L << 20));
+ try (DataFrame df = ctx.sql("SELECT COUNT(*) FROM t");
+ ArrowReader reader = df.collect(allocator)) {
+ assertTrue(reader.loadNextBatch());
+ BigIntVector count = (BigIntVector) reader.getVectorSchemaRoot().getVector(0);
+ assertEquals(6_001_215L, count.get(0));
+ }
+ }
+ }
+}