This is an automated email from the ASF dual-hosted git repository.

andygrove pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/datafusion-java.git


The following commit(s) were added to refs/heads/main by this push:
     new 63a86a5  feat(json): expose NdJsonReadOptions via registerJson and 
readJson (#47)
63a86a5 is described below

commit 63a86a588ab274a796784373d5d5519b9953b71c
Author: Lantao Jin <[email protected]>
AuthorDate: Mon May 18 05:00:28 2026 +0800

    feat(json): expose NdJsonReadOptions via registerJson and readJson (#47)
---
 .../java/org/apache/datafusion/CsvReadOptions.java |  32 +----
 .../org/apache/datafusion/FileCompressionType.java |  33 +++++
 .../apache/datafusion/FileCompressionTypes.java    |  47 +++++++
 .../org/apache/datafusion/NdJsonReadOptions.java   |  78 +++++++++++
 .../java/org/apache/datafusion/SessionContext.java |  67 ++++++++++
 .../org/apache/datafusion/CsvReadOptionsTest.java  |  23 +++-
 ...OptionsTest.java => NdJsonReadOptionsTest.java} |  75 +++++------
 .../apache/datafusion/SessionContextJsonTest.java  | 148 +++++++++++++++++++++
 native/build.rs                                    |   2 +
 native/src/json.rs                                 | 116 ++++++++++++++++
 native/src/lib.rs                                  |   1 +
 proto/csv_read_options.proto                       |  11 +-
 ...d_options.proto => file_compression_type.proto} |  21 +--
 native/build.rs => proto/json_read_options.proto   |  31 +++--
 14 files changed, 573 insertions(+), 112 deletions(-)

diff --git a/core/src/main/java/org/apache/datafusion/CsvReadOptions.java 
b/core/src/main/java/org/apache/datafusion/CsvReadOptions.java
index f441820..ce62afe 100644
--- a/core/src/main/java/org/apache/datafusion/CsvReadOptions.java
+++ b/core/src/main/java/org/apache/datafusion/CsvReadOptions.java
@@ -33,15 +33,6 @@ import org.apache.arrow.vector.types.pojo.Schema;
  */
 public final class CsvReadOptions {
 
-  /** Compression of the file. Names match DataFusion's {@code 
FileCompressionType} variants. */
-  public enum FileCompressionType {
-    UNCOMPRESSED,
-    GZIP,
-    BZIP2,
-    XZ,
-    ZSTD
-  }
-
   private boolean hasHeader = true;
   private byte delimiter = (byte) ',';
   private byte quote = (byte) '"';
@@ -90,6 +81,9 @@ public final class CsvReadOptions {
   }
 
   public CsvReadOptions schemaInferMaxRecords(long n) {
+    if (n < 0) {
+      throw new IllegalArgumentException("schemaInferMaxRecords must be 
non-negative, got " + n);
+    }
     this.schemaInferMaxRecords = n;
     return this;
   }
@@ -116,7 +110,7 @@ public final class CsvReadOptions {
             .setDelimiter(delimiter & 0xFF)
             .setQuote(quote & 0xFF)
             .setFileExtension(fileExtension)
-            .setFileCompressionType(toProto(fileCompressionType));
+            
.setFileCompressionType(FileCompressionTypes.toProto(fileCompressionType));
     if (terminator != null) {
       b.setTerminator(terminator & 0xFF);
     }
@@ -138,22 +132,4 @@ public final class CsvReadOptions {
   Schema schema() {
     return schema;
   }
-
-  private static org.apache.datafusion.protobuf.FileCompressionType 
toProto(FileCompressionType t) {
-    switch (t) {
-      case UNCOMPRESSED:
-        return org.apache.datafusion.protobuf.FileCompressionType
-            .FILE_COMPRESSION_TYPE_UNCOMPRESSED;
-      case GZIP:
-        return 
org.apache.datafusion.protobuf.FileCompressionType.FILE_COMPRESSION_TYPE_GZIP;
-      case BZIP2:
-        return 
org.apache.datafusion.protobuf.FileCompressionType.FILE_COMPRESSION_TYPE_BZIP2;
-      case XZ:
-        return 
org.apache.datafusion.protobuf.FileCompressionType.FILE_COMPRESSION_TYPE_XZ;
-      case ZSTD:
-        return 
org.apache.datafusion.protobuf.FileCompressionType.FILE_COMPRESSION_TYPE_ZSTD;
-      default:
-        throw new IllegalArgumentException("unhandled FileCompressionType: " + 
t);
-    }
-  }
 }
diff --git a/core/src/main/java/org/apache/datafusion/FileCompressionType.java 
b/core/src/main/java/org/apache/datafusion/FileCompressionType.java
new file mode 100644
index 0000000..cae1a9c
--- /dev/null
+++ b/core/src/main/java/org/apache/datafusion/FileCompressionType.java
@@ -0,0 +1,33 @@
+/*
+ * 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;
+
+/**
+ * Compression of a file-format source. Shared by {@link CsvReadOptions} and 
{@link
+ * NdJsonReadOptions} (and any future format that exposes the same set of 
compressions). Variant
+ * names match DataFusion's Rust {@code FileCompressionType} so they 
round-trip across JNI.
+ */
+public enum FileCompressionType {
+  UNCOMPRESSED,
+  GZIP,
+  BZIP2,
+  XZ,
+  ZSTD
+}
diff --git a/core/src/main/java/org/apache/datafusion/FileCompressionTypes.java 
b/core/src/main/java/org/apache/datafusion/FileCompressionTypes.java
new file mode 100644
index 0000000..2e7ea56
--- /dev/null
+++ b/core/src/main/java/org/apache/datafusion/FileCompressionTypes.java
@@ -0,0 +1,47 @@
+/*
+ * 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;
+
+/**
+ * Internal helpers for translating between the user-facing {@link 
FileCompressionType} enum and the
+ * protobuf-generated {@code 
org.apache.datafusion.protobuf.FileCompressionType} enum used on the
+ * wire. Same variant set, different Java types.
+ */
+final class FileCompressionTypes {
+  private FileCompressionTypes() {}
+
+  static org.apache.datafusion.protobuf.FileCompressionType 
toProto(FileCompressionType t) {
+    switch (t) {
+      case UNCOMPRESSED:
+        return org.apache.datafusion.protobuf.FileCompressionType
+            .FILE_COMPRESSION_TYPE_UNCOMPRESSED;
+      case GZIP:
+        return 
org.apache.datafusion.protobuf.FileCompressionType.FILE_COMPRESSION_TYPE_GZIP;
+      case BZIP2:
+        return 
org.apache.datafusion.protobuf.FileCompressionType.FILE_COMPRESSION_TYPE_BZIP2;
+      case XZ:
+        return 
org.apache.datafusion.protobuf.FileCompressionType.FILE_COMPRESSION_TYPE_XZ;
+      case ZSTD:
+        return 
org.apache.datafusion.protobuf.FileCompressionType.FILE_COMPRESSION_TYPE_ZSTD;
+      default:
+        throw new IllegalArgumentException("unhandled FileCompressionType: " + 
t);
+    }
+  }
+}
diff --git a/core/src/main/java/org/apache/datafusion/NdJsonReadOptions.java 
b/core/src/main/java/org/apache/datafusion/NdJsonReadOptions.java
new file mode 100644
index 0000000..1fa1bf1
--- /dev/null
+++ b/core/src/main/java/org/apache/datafusion/NdJsonReadOptions.java
@@ -0,0 +1,78 @@
+/*
+ * 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 newline-delimited JSON sources passed to {@link
+ * SessionContext#registerJson(String, String, NdJsonReadOptions)} and {@link
+ * SessionContext#readJson(String, NdJsonReadOptions)}.
+ *
+ * <p>Mirrors a subset of DataFusion's {@code NdJsonReadOptions}. All setters 
return {@code this}
+ * for fluent chaining. Defaults match the Rust struct: {@code fileExtension = 
".json"}, {@code
+ * fileCompressionType = UNCOMPRESSED}; {@code schemaInferMaxRecords} unset 
(the DataFusion default
+ * is used).
+ */
+public final class NdJsonReadOptions {
+
+  private String fileExtension = ".json";
+  private FileCompressionType fileCompressionType = 
FileCompressionType.UNCOMPRESSED;
+  private Long schemaInferMaxRecords;
+  private Schema schema;
+
+  public NdJsonReadOptions fileExtension(String ext) {
+    this.fileExtension = ext;
+    return this;
+  }
+
+  public NdJsonReadOptions fileCompressionType(FileCompressionType t) {
+    this.fileCompressionType = t;
+    return this;
+  }
+
+  public NdJsonReadOptions schemaInferMaxRecords(long n) {
+    if (n < 0) {
+      throw new IllegalArgumentException("schemaInferMaxRecords must be 
non-negative, got " + n);
+    }
+    this.schemaInferMaxRecords = n;
+    return this;
+  }
+
+  public NdJsonReadOptions schema(Schema schema) {
+    this.schema = schema;
+    return this;
+  }
+
+  byte[] toBytes() {
+    org.apache.datafusion.protobuf.NdJsonReadOptionsProto.Builder b =
+        org.apache.datafusion.protobuf.NdJsonReadOptionsProto.newBuilder()
+            .setFileExtension(fileExtension)
+            
.setFileCompressionType(FileCompressionTypes.toProto(fileCompressionType));
+    if (schemaInferMaxRecords != null) {
+      b.setSchemaInferMaxRecords(schemaInferMaxRecords);
+    }
+    return b.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 1b2a075..51384a6 100644
--- a/core/src/main/java/org/apache/datafusion/SessionContext.java
+++ b/core/src/main/java/org/apache/datafusion/SessionContext.java
@@ -188,6 +188,67 @@ public final class SessionContext implements AutoCloseable 
{
     return new DataFrame(dfHandle);
   }
 
+  public void registerJson(String name, String path) {
+    registerJson(name, path, new NdJsonReadOptions());
+  }
+
+  /**
+   * Register a newline-delimited JSON file (or directory of NDJSON files) as 
a table with the
+   * supplied {@link NdJsonReadOptions}.
+   *
+   * @throws RuntimeException if registration fails (path not found, schema 
inference error, etc.).
+   */
+  public void registerJson(String name, String path, NdJsonReadOptions 
options) {
+    if (nativeHandle == 0) {
+      throw new IllegalStateException("SessionContext is closed");
+    }
+    if (name == null) {
+      throw new IllegalArgumentException("registerJson name must be non-null");
+    }
+    if (path == null) {
+      throw new IllegalArgumentException("registerJson path must be non-null");
+    }
+    if (options == null) {
+      throw new IllegalArgumentException("registerJson options must be 
non-null");
+    }
+    registerJsonWithOptions(
+        nativeHandle,
+        name,
+        path,
+        options.toBytes(),
+        options.schema() != null ? serializeSchemaIpc(options.schema()) : 
null);
+  }
+
+  /** Read a newline-delimited JSON file as a {@link DataFrame} without 
registering it. */
+  public DataFrame readJson(String path) {
+    return readJson(path, new NdJsonReadOptions());
+  }
+
+  /**
+   * Read a newline-delimited JSON file as a {@link DataFrame} with the 
supplied {@link
+   * NdJsonReadOptions}.
+   *
+   * @throws RuntimeException if the read fails.
+   */
+  public DataFrame readJson(String path, NdJsonReadOptions options) {
+    if (nativeHandle == 0) {
+      throw new IllegalStateException("SessionContext is closed");
+    }
+    if (path == null) {
+      throw new IllegalArgumentException("readJson path must be non-null");
+    }
+    if (options == null) {
+      throw new IllegalArgumentException("readJson options must be non-null");
+    }
+    long dfHandle =
+        readJsonWithOptions(
+            nativeHandle,
+            path,
+            options.toBytes(),
+            options.schema() != null ? serializeSchemaIpc(options.schema()) : 
null);
+    return new DataFrame(dfHandle);
+  }
+
   public void registerParquet(String name, String path) {
     registerParquet(name, path, new ParquetReadOptions());
   }
@@ -277,5 +338,11 @@ public final class SessionContext implements AutoCloseable 
{
   private static native long readCsvWithOptions(
       long handle, String path, byte[] optionsBytes, byte[] schemaIpcBytes);
 
+  private static native void registerJsonWithOptions(
+      long handle, String name, String path, byte[] optionsBytes, byte[] 
schemaIpcBytes);
+
+  private static native long readJsonWithOptions(
+      long handle, String path, byte[] optionsBytes, byte[] schemaIpcBytes);
+
   private static native void closeSessionContext(long handle);
 }
diff --git a/core/src/test/java/org/apache/datafusion/CsvReadOptionsTest.java 
b/core/src/test/java/org/apache/datafusion/CsvReadOptionsTest.java
index 72f8e20..2e35522 100644
--- a/core/src/test/java/org/apache/datafusion/CsvReadOptionsTest.java
+++ b/core/src/test/java/org/apache/datafusion/CsvReadOptionsTest.java
@@ -22,6 +22,7 @@ package org.apache.datafusion;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
 import java.util.List;
@@ -31,7 +32,6 @@ 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.CsvReadOptionsProto;
-import org.apache.datafusion.protobuf.FileCompressionType;
 import org.junit.jupiter.api.Test;
 
 import com.google.protobuf.InvalidProtocolBufferException;
@@ -47,7 +47,8 @@ class CsvReadOptionsTest {
     assertEquals((int) '"', p.getQuote());
     assertEquals(".csv", p.getFileExtension());
     assertEquals(
-        FileCompressionType.FILE_COMPRESSION_TYPE_UNCOMPRESSED, 
p.getFileCompressionType());
+        
org.apache.datafusion.protobuf.FileCompressionType.FILE_COMPRESSION_TYPE_UNCOMPRESSED,
+        p.getFileCompressionType());
 
     assertFalse(p.hasTerminator());
     assertFalse(p.hasEscape());
@@ -69,7 +70,7 @@ class CsvReadOptionsTest {
             .newlinesInValues(true)
             .schemaInferMaxRecords(10L)
             .fileExtension(".tsv")
-            .fileCompressionType(CsvReadOptions.FileCompressionType.GZIP);
+            .fileCompressionType(FileCompressionType.GZIP);
 
     CsvReadOptionsProto p = CsvReadOptionsProto.parseFrom(opts.toBytes());
 
@@ -82,7 +83,9 @@ class CsvReadOptionsTest {
     assertTrue(p.getNewlinesInValues());
     assertEquals(10L, p.getSchemaInferMaxRecords());
     assertEquals(".tsv", p.getFileExtension());
-    assertEquals(FileCompressionType.FILE_COMPRESSION_TYPE_GZIP, 
p.getFileCompressionType());
+    assertEquals(
+        
org.apache.datafusion.protobuf.FileCompressionType.FILE_COMPRESSION_TYPE_GZIP,
+        p.getFileCompressionType());
   }
 
   @Test
@@ -96,7 +99,7 @@ class CsvReadOptionsTest {
 
   @Test
   void allCompressionTypesMapThroughProto() throws 
InvalidProtocolBufferException {
-    for (CsvReadOptions.FileCompressionType t : 
CsvReadOptions.FileCompressionType.values()) {
+    for (FileCompressionType t : FileCompressionType.values()) {
       CsvReadOptionsProto p =
           CsvReadOptionsProto.parseFrom(new 
CsvReadOptions().fileCompressionType(t).toBytes());
       assertEquals(
@@ -105,4 +108,14 @@ class CsvReadOptionsTest {
           "mismatch for " + t);
     }
   }
+
+  @Test
+  void schemaInferMaxRecordsRejectsNegative() {
+    // The proto wire field is uint64, so a negative long would be 
reinterpreted
+    // as a huge unsigned value on the Rust side and silently expand schema
+    // inference across the full dataset. Reject at the Java setter instead.
+    CsvReadOptions opts = new CsvReadOptions();
+    assertThrows(IllegalArgumentException.class, () -> 
opts.schemaInferMaxRecords(-1L));
+    assertThrows(IllegalArgumentException.class, () -> 
opts.schemaInferMaxRecords(Long.MIN_VALUE));
+  }
 }
diff --git a/core/src/test/java/org/apache/datafusion/CsvReadOptionsTest.java 
b/core/src/test/java/org/apache/datafusion/NdJsonReadOptionsTest.java
similarity index 50%
copy from core/src/test/java/org/apache/datafusion/CsvReadOptionsTest.java
copy to core/src/test/java/org/apache/datafusion/NdJsonReadOptionsTest.java
index 72f8e20..f55551e 100644
--- a/core/src/test/java/org/apache/datafusion/CsvReadOptionsTest.java
+++ b/core/src/test/java/org/apache/datafusion/NdJsonReadOptionsTest.java
@@ -22,7 +22,7 @@ package org.apache.datafusion;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertSame;
-import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.assertThrows;
 
 import java.util.List;
 
@@ -30,79 +30,70 @@ 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.CsvReadOptionsProto;
-import org.apache.datafusion.protobuf.FileCompressionType;
+import org.apache.datafusion.protobuf.NdJsonReadOptionsProto;
 import org.junit.jupiter.api.Test;
 
 import com.google.protobuf.InvalidProtocolBufferException;
 
-class CsvReadOptionsTest {
+class NdJsonReadOptionsTest {
 
   @Test
   void defaultsRoundTripThroughProto() throws InvalidProtocolBufferException {
-    CsvReadOptionsProto p = CsvReadOptionsProto.parseFrom(new 
CsvReadOptions().toBytes());
+    NdJsonReadOptionsProto p = NdJsonReadOptionsProto.parseFrom(new 
NdJsonReadOptions().toBytes());
 
-    assertTrue(p.getHasHeader());
-    assertEquals((int) ',', p.getDelimiter());
-    assertEquals((int) '"', p.getQuote());
-    assertEquals(".csv", p.getFileExtension());
+    assertEquals(".json", p.getFileExtension());
     assertEquals(
-        FileCompressionType.FILE_COMPRESSION_TYPE_UNCOMPRESSED, 
p.getFileCompressionType());
-
-    assertFalse(p.hasTerminator());
-    assertFalse(p.hasEscape());
-    assertFalse(p.hasComment());
-    assertFalse(p.hasNewlinesInValues());
+        
org.apache.datafusion.protobuf.FileCompressionType.FILE_COMPRESSION_TYPE_UNCOMPRESSED,
+        p.getFileCompressionType());
     assertFalse(p.hasSchemaInferMaxRecords());
   }
 
   @Test
   void fullyConfiguredRoundTripsThroughProto() throws 
InvalidProtocolBufferException {
-    CsvReadOptions opts =
-        new CsvReadOptions()
-            .hasHeader(false)
-            .delimiter((byte) '|')
-            .quote((byte) '\'')
-            .terminator((byte) '\n')
-            .escape((byte) '\\')
-            .comment((byte) '#')
-            .newlinesInValues(true)
-            .schemaInferMaxRecords(10L)
-            .fileExtension(".tsv")
-            .fileCompressionType(CsvReadOptions.FileCompressionType.GZIP);
+    NdJsonReadOptions opts =
+        new NdJsonReadOptions()
+            .fileExtension(".ndjson")
+            .fileCompressionType(FileCompressionType.GZIP)
+            .schemaInferMaxRecords(50L);
 
-    CsvReadOptionsProto p = CsvReadOptionsProto.parseFrom(opts.toBytes());
+    NdJsonReadOptionsProto p = 
NdJsonReadOptionsProto.parseFrom(opts.toBytes());
 
-    assertFalse(p.getHasHeader());
-    assertEquals((int) '|', p.getDelimiter());
-    assertEquals((int) '\'', p.getQuote());
-    assertEquals((int) '\n', p.getTerminator());
-    assertEquals((int) '\\', p.getEscape());
-    assertEquals((int) '#', p.getComment());
-    assertTrue(p.getNewlinesInValues());
-    assertEquals(10L, p.getSchemaInferMaxRecords());
-    assertEquals(".tsv", p.getFileExtension());
-    assertEquals(FileCompressionType.FILE_COMPRESSION_TYPE_GZIP, 
p.getFileCompressionType());
+    assertEquals(".ndjson", p.getFileExtension());
+    assertEquals(
+        
org.apache.datafusion.protobuf.FileCompressionType.FILE_COMPRESSION_TYPE_GZIP,
+        p.getFileCompressionType());
+    assertEquals(50L, p.getSchemaInferMaxRecords());
   }
 
   @Test
   void schemaIsHeldByReferenceAndNotInProto() {
     Schema schema =
         new Schema(List.of(new Field("x", FieldType.nullable(new 
ArrowType.Int(32, true)), null)));
-    CsvReadOptions opts = new CsvReadOptions().schema(schema);
+    NdJsonReadOptions opts = new NdJsonReadOptions().schema(schema);
 
     assertSame(schema, opts.schema());
   }
 
   @Test
   void allCompressionTypesMapThroughProto() throws 
InvalidProtocolBufferException {
-    for (CsvReadOptions.FileCompressionType t : 
CsvReadOptions.FileCompressionType.values()) {
-      CsvReadOptionsProto p =
-          CsvReadOptionsProto.parseFrom(new 
CsvReadOptions().fileCompressionType(t).toBytes());
+    for (FileCompressionType t : FileCompressionType.values()) {
+      NdJsonReadOptionsProto p =
+          NdJsonReadOptionsProto.parseFrom(
+              new NdJsonReadOptions().fileCompressionType(t).toBytes());
       assertEquals(
           "FILE_COMPRESSION_TYPE_" + t.name(),
           p.getFileCompressionType().name(),
           "mismatch for " + t);
     }
   }
+
+  @Test
+  void schemaInferMaxRecordsRejectsNegative() {
+    // The proto wire field is uint64, so a negative long would be 
reinterpreted
+    // as a huge unsigned value on the Rust side and silently expand schema
+    // inference across the full dataset. Reject at the Java setter instead.
+    NdJsonReadOptions opts = new NdJsonReadOptions();
+    assertThrows(IllegalArgumentException.class, () -> 
opts.schemaInferMaxRecords(-1L));
+    assertThrows(IllegalArgumentException.class, () -> 
opts.schemaInferMaxRecords(Long.MIN_VALUE));
+  }
 }
diff --git 
a/core/src/test/java/org/apache/datafusion/SessionContextJsonTest.java 
b/core/src/test/java/org/apache/datafusion/SessionContextJsonTest.java
new file mode 100644
index 0000000..20c57b1
--- /dev/null
+++ b/core/src/test/java/org/apache/datafusion/SessionContextJsonTest.java
@@ -0,0 +1,148 @@
+/*
+ * 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.file.Files;
+import java.nio.file.Path;
+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.VarCharVector;
+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.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+class SessionContextJsonTest {
+
+  private static Path writeJson(Path dir, String name, String contents) throws 
IOException {
+    Path file = dir.resolve(name);
+    Files.writeString(file, contents);
+    return file;
+  }
+
+  @Test
+  void registerJsonInfersSchemaAndCounts(@TempDir Path tempDir) throws 
Exception {
+    Path file =
+        writeJson(
+            tempDir,
+            "people.json",
+            "{\"id\":1,\"name\":\"alice\"}\n"
+                + "{\"id\":2,\"name\":\"bob\"}\n"
+                + "{\"id\":3,\"name\":\"carol\"}\n");
+
+    try (BufferAllocator allocator = new RootAllocator();
+        SessionContext ctx = new SessionContext()) {
+      ctx.registerJson("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 readJsonWithExplicitSchema(@TempDir Path tempDir) throws Exception {
+    Path file =
+        writeJson(
+            tempDir, "headerless.json", 
"{\"id\":10,\"name\":\"x\"}\n{\"id\":20,\"name\":\"y\"}\n");
+
+    Schema schema =
+        new Schema(
+            List.of(
+                new Field("id", FieldType.nullable(new ArrowType.Int(64, 
true)), null),
+                new Field("name", FieldType.nullable(new ArrowType.Utf8()), 
null)));
+
+    try (BufferAllocator allocator = new RootAllocator();
+        SessionContext ctx = new SessionContext();
+        DataFrame df =
+            ctx.readJson(file.toAbsolutePath().toString(), new 
NdJsonReadOptions().schema(schema));
+        ArrowReader reader = df.collect(allocator)) {
+      assertTrue(reader.loadNextBatch());
+      VectorSchemaRoot root = reader.getVectorSchemaRoot();
+      assertEquals(2, root.getRowCount());
+      assertEquals("id", root.getSchema().getFields().get(0).getName());
+      assertEquals("name", root.getSchema().getFields().get(1).getName());
+    }
+  }
+
+  @Test
+  void registerJsonWithCustomExtension(@TempDir Path tempDir) throws Exception 
{
+    Path file = writeJson(tempDir, "data.ndjson", 
"{\"x\":10,\"y\":20}\n{\"x\":30,\"y\":40}\n");
+
+    try (BufferAllocator allocator = new RootAllocator();
+        SessionContext ctx = new SessionContext()) {
+      ctx.registerJson(
+          "t", file.toAbsolutePath().toString(), new 
NdJsonReadOptions().fileExtension(".ndjson"));
+
+      try (DataFrame df = ctx.sql("SELECT SUM(x) + SUM(y) FROM t");
+          ArrowReader reader = df.collect(allocator)) {
+        assertTrue(reader.loadNextBatch());
+        BigIntVector v = (BigIntVector) 
reader.getVectorSchemaRoot().getVector(0);
+        assertEquals(100L, v.get(0));
+      }
+    }
+  }
+
+  @Test
+  void registerJsonRejectsNullArguments() {
+    try (SessionContext ctx = new SessionContext()) {
+      NdJsonReadOptions opts = new NdJsonReadOptions();
+      assertThrows(IllegalArgumentException.class, () -> 
ctx.registerJson(null, "/p"));
+      assertThrows(IllegalArgumentException.class, () -> ctx.registerJson("t", 
null));
+      assertThrows(IllegalArgumentException.class, () -> 
ctx.registerJson(null, "/p", opts));
+      assertThrows(IllegalArgumentException.class, () -> ctx.registerJson("t", 
null, opts));
+      assertThrows(IllegalArgumentException.class, () -> ctx.registerJson("t", 
"/p", null));
+    }
+  }
+
+  @Test
+  void readJsonRejectsNullArguments() {
+    try (SessionContext ctx = new SessionContext()) {
+      NdJsonReadOptions opts = new NdJsonReadOptions();
+      assertThrows(IllegalArgumentException.class, () -> ctx.readJson(null));
+      assertThrows(IllegalArgumentException.class, () -> ctx.readJson(null, 
opts));
+      assertThrows(IllegalArgumentException.class, () -> ctx.readJson("/p", 
null));
+    }
+  }
+}
diff --git a/native/build.rs b/native/build.rs
index 5a27cb0..7c164d5 100644
--- a/native/build.rs
+++ b/native/build.rs
@@ -18,7 +18,9 @@
 fn main() {
     const PROTOS: &[&str] = &[
         "../proto/session_options.proto",
+        "../proto/file_compression_type.proto",
         "../proto/csv_read_options.proto",
+        "../proto/json_read_options.proto",
         "../proto/parquet_read_options.proto",
     ];
     for p in PROTOS {
diff --git a/native/src/json.rs b/native/src/json.rs
new file mode 100644
index 0000000..43f0055
--- /dev/null
+++ b/native/src/json.rs
@@ -0,0 +1,116 @@
+// 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::datasource::file_format::file_compression_type::FileCompressionType;
+use datafusion::error::DataFusionError;
+use datafusion::prelude::JsonReadOptions;
+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::{FileCompressionType as ProtoFileCompressionType, 
NdJsonReadOptionsProto};
+use crate::runtime;
+use crate::schema::decode_optional_schema;
+
+fn with_json_options<R>(
+    env: &mut JNIEnv,
+    options_bytes: JByteArray,
+    schema_ipc_bytes: JByteArray,
+    f: impl FnOnce(JsonReadOptions) -> JniResult<R>,
+) -> JniResult<R> {
+    let bytes: Vec<u8> = env.convert_byte_array(&options_bytes)?;
+    let p = NdJsonReadOptionsProto::decode(bytes.as_slice())?;
+
+    let schema = decode_optional_schema(env, schema_ipc_bytes)?;
+
+    let compression = match p.file_compression_type() {
+        ProtoFileCompressionType::Unspecified => {
+            return Err("NdJsonReadOptionsProto.file_compression_type is 
UNSPECIFIED".into());
+        }
+        ProtoFileCompressionType::Uncompressed => 
FileCompressionType::UNCOMPRESSED,
+        ProtoFileCompressionType::Gzip => FileCompressionType::GZIP,
+        ProtoFileCompressionType::Bzip2 => FileCompressionType::BZIP2,
+        ProtoFileCompressionType::Xz => FileCompressionType::XZ,
+        ProtoFileCompressionType::Zstd => FileCompressionType::ZSTD,
+    };
+
+    let file_ext = p.file_extension;
+    let mut opts = JsonReadOptions::default()
+        .file_extension(&file_ext)
+        .file_compression_type(compression);
+
+    if let Some(n) = p.schema_infer_max_records {
+        opts = opts.schema_infer_max_records(n 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_registerJsonWithOptions<'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_json_options(env, options_bytes, schema_ipc_bytes, |opts| {
+            runtime().block_on(async {
+                ctx.register_json(&name, &path, opts).await?;
+                Ok::<(), DataFusionError>(())
+            })?;
+            Ok(())
+        })
+    })
+}
+
+#[no_mangle]
+pub extern "system" fn 
Java_org_apache_datafusion_SessionContext_readJsonWithOptions<'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_json_options(env, options_bytes, schema_ipc_bytes, |opts| {
+            let df = runtime().block_on(ctx.read_json(path, opts))?;
+            Ok(Box::into_raw(Box::new(df)) as jlong)
+        })
+    })
+}
diff --git a/native/src/lib.rs b/native/src/lib.rs
index 9041819..efd7801 100644
--- a/native/src/lib.rs
+++ b/native/src/lib.rs
@@ -17,6 +17,7 @@
 
 mod csv;
 mod errors;
+mod json;
 mod proto;
 mod schema;
 
diff --git a/proto/csv_read_options.proto b/proto/csv_read_options.proto
index 840867d..378c92a 100644
--- a/proto/csv_read_options.proto
+++ b/proto/csv_read_options.proto
@@ -19,6 +19,8 @@ syntax = "proto3";
 
 package datafusion_java;
 
+import "file_compression_type.proto";
+
 option java_package = "org.apache.datafusion.protobuf";
 option java_multiple_files = true;
 
@@ -37,12 +39,3 @@ message CsvReadOptionsProto {
   string file_extension = 9;
   FileCompressionType file_compression_type = 10;
 }
-
-enum FileCompressionType {
-  FILE_COMPRESSION_TYPE_UNSPECIFIED = 0;
-  FILE_COMPRESSION_TYPE_UNCOMPRESSED = 1;
-  FILE_COMPRESSION_TYPE_GZIP = 2;
-  FILE_COMPRESSION_TYPE_BZIP2 = 3;
-  FILE_COMPRESSION_TYPE_XZ = 4;
-  FILE_COMPRESSION_TYPE_ZSTD = 5;
-}
diff --git a/proto/csv_read_options.proto b/proto/file_compression_type.proto
similarity index 65%
copy from proto/csv_read_options.proto
copy to proto/file_compression_type.proto
index 840867d..e02593f 100644
--- a/proto/csv_read_options.proto
+++ b/proto/file_compression_type.proto
@@ -22,22 +22,11 @@ package datafusion_java;
 option java_package = "org.apache.datafusion.protobuf";
 option java_multiple_files = true;
 
-// Options used to read CSV files. Fields with non-null Java defaults are
-// always sent; fields marked `optional` preserve unset-ness so the Rust
-// side can leave a DataFusion default in place.
-message CsvReadOptionsProto {
-  bool has_header = 1;
-  uint32 delimiter = 2;            // single byte, sent as uint32
-  uint32 quote = 3;                // single byte
-  optional uint32 terminator = 4;
-  optional uint32 escape = 5;
-  optional uint32 comment = 6;
-  optional bool newlines_in_values = 7;
-  optional uint64 schema_infer_max_records = 8;
-  string file_extension = 9;
-  FileCompressionType file_compression_type = 10;
-}
-
+// Compression of a file-format source. Shared by `csv_read_options.proto`
+// and `json_read_options.proto` (and any future format that exposes the
+// same set of compressions). Variants and tag numbers must match
+// DataFusion's `FileCompressionType` so the JNI dispatch can map values
+// directly without a translation table.
 enum FileCompressionType {
   FILE_COMPRESSION_TYPE_UNSPECIFIED = 0;
   FILE_COMPRESSION_TYPE_UNCOMPRESSED = 1;
diff --git a/native/build.rs b/proto/json_read_options.proto
similarity index 52%
copy from native/build.rs
copy to proto/json_read_options.proto
index 5a27cb0..4a64f4d 100644
--- a/native/build.rs
+++ b/proto/json_read_options.proto
@@ -15,16 +15,23 @@
 // specific language governing permissions and limitations
 // under the License.
 
-fn main() {
-    const PROTOS: &[&str] = &[
-        "../proto/session_options.proto",
-        "../proto/csv_read_options.proto",
-        "../proto/parquet_read_options.proto",
-    ];
-    for p in PROTOS {
-        println!("cargo:rerun-if-changed={p}");
-    }
-    let protoc = protoc_bin_vendored::protoc_bin_path().expect("vendored 
protoc not available");
-    std::env::set_var("PROTOC", protoc);
-    prost_build::compile_protos(PROTOS, &["../proto"]).expect("failed to 
compile protos");
+syntax = "proto3";
+
+package datafusion_java;
+
+import "file_compression_type.proto";
+
+option java_package = "org.apache.datafusion.protobuf";
+option java_multiple_files = true;
+
+// Options used to read newline-delimited JSON (NDJSON) files. `file_extension`
+// has a non-null Java default and is always sent; the rest preserve unset-ness
+// so the Rust side can leave a DataFusion default in place. The shared
+// `FileCompressionType` enum lives in its own proto file so both this and
+// csv_read_options.proto can reference it without one file depending on the
+// other.
+message NdJsonReadOptionsProto {
+  string file_extension = 1;
+  FileCompressionType file_compression_type = 2;
+  optional uint64 schema_infer_max_records = 3;
 }


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]


Reply via email to